Zum Inhalt springen
SHIELDMYSERVER

Dienste einsperren: mit systemd, ohne Zusatzsoftware

ProtectSystem, PrivateTmp, NoNewPrivileges und Co.: eine Sandbox je Dienst, der Weg dorthin über systemd-analyze security und die zwei Schalter, die Dienste brechen.

mittelDiesen Monat. Wichtig, aber kein Feuer.

veröffentlicht 18.08.2026 · 3 min

Titelbild: Dienste einsperren: mit systemd, ohne Zusatzsoftware

Ein Dienst, der als eigener Benutzer läuft, ist gut. Ein Dienst, der als eigener Benutzer läuft und trotzdem /etc lesen, überall schreiben und beliebig ins Netz darf, nutzt die Hälfte der Möglichkeiten nicht. systemd bringt die andere Hälfte mit, ohne AppArmor-Profil, ohne Container, ohne zusätzliches Paket.

Warum mittel

Das ist Tiefenverteidigung: Es verhindert keinen Einbruch, sondern begrenzt, was danach möglich ist. Wichtig, aber nachrangig gegenüber offenen Oberflächen und fehlenden Updates und es ist an einem Nachmittag erledigt.

Der Ausgangspunkt#

# Bewertung aller Dienste, schlechteste zuerst
systemd-analyze security

# Ein Dienst im Detail: welche Schraube fehlt?
systemd-analyze security app.service

Die Ausgabe listet jede Einstellung mit Wirkung und aktuellem Zustand. Sie ist keine Note für Sicherheit im Ganzen, ein Dienst mit gutem Wert kann trotzdem eine verwundbare Anwendung sein. Sie ist eine Liste dessen, was ohne Aufwand zu haben wäre.

Ein Drop-in statt Bearbeitung der Unit#

Die Unit aus dem Paket wird beim nächsten Update überschrieben. Änderungen gehören daneben:

sudo systemctl edit app.service     # legt /etc/systemd/system/app.service.d/override.conf an
[Service]
# Konto und Rechte
User=app
Group=app
NoNewPrivileges=yes
CapabilityBoundingSet=
AmbientCapabilities=

# Dateisystem
ProtectSystem=strict          # alles außer /dev, /proc, /sys nur lesbar
ProtectHome=yes
PrivateTmp=yes
StateDirectory=app            # /var/lib/app, schreibbar, gehört dem Dienst
ReadWritePaths=/var/log/app

# Kernel und Prozesse
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectProc=invisible
RestrictSUIDSGID=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes    # bricht JIT-Sprachen — vorher prüfen

# Netz
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
IPAddressDeny=any
IPAddressAllow=localhost 10.99.0.0/24

# Systemaufrufe
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources
sudo systemctl daemon-reload && sudo systemctl restart app.service
systemctl status app.service --no-pager
Zwei Einstellungen, die häufig etwas kaputtmachen

MemoryDenyWriteExecute=yes bricht alles mit Just-in-time-Übersetzung (Java, Node.js, PHP mit JIT, viele Python-Erweiterungen). IPAddressDeny=any schneidet DNS ab, wenn der Auflöser nicht in IPAddressAllow steht. Beides erzeugt Fehler, die nicht nach Sandbox aussehen, deshalb einzeln einschalten und nach jedem Schritt neu starten.

Die Reihenfolge, die keinen Ausfall erzeugt#

  1. Bewertung notieren: systemd-analyze security app.service > /tmp/vorher.txt
  2. Nur die ungefährlichen Einstellungen setzen: NoNewPrivileges, PrivateTmp, ProtectHome, ProtectKernel*, ProtectControlGroups.
  3. Neu starten, Funktion prüfen, nicht nur active (running), sondern einen echten Aufruf.
  4. ProtectSystem=strict plus StateDirectory/ReadWritePaths ergänzen. Hier fällt auf, wohin der Dienst tatsächlich schreibt.
  5. Netz- und Systemaufruf-Filter zuletzt, mit Blick ins Journal:
journalctl -u app.service -f
# Sandbox-Treffer sind meist "Operation not permitted" oder "Permission denied"
# an Stellen, die vorher funktioniert haben

Was der Sandbox entgeht#

Ein Dienst, der eine Datenbank erreichen darf, kann sie auch leeren. Ein Dienst, der ins Internet darf, kann nachladen. Die Sandbox begrenzt Reichweite, nicht Absicht, die Ergänzung dazu sind ausgehende Regeln und Rechte, die schon vorher eng sind (Rechte, die nur so weit reichen wie nötig).

Beispiel: ein statischer Webdienst#

[Service]
User=web
DynamicUser=yes               # Konto entsteht beim Start, existiert sonst nicht
ProtectSystem=strict
ProtectHome=yes
PrivateDevices=yes
NoNewPrivileges=yes
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
SystemCallFilter=@system-service
RestrictAddressFamilies=AF_INET AF_INET6

DynamicUser=yes ist die eleganteste Variante, wo sie passt: Der Benutzer existiert nur während der Laufzeit, Zustandsverzeichnisse werden automatisch angelegt und gehören ihm. Wo dauerhaft Dateien geteilt werden müssen, passt sie nicht, dann bleibt es beim festen Konto.

Ein Dienst, der so eingesperrt ist, hinterlässt bei einer Übernahme deutlich mehr Spuren, weil ungewöhnliche Aufrufe scheitern und im Journal landen. Damit diese Spuren den Vorfall überleben, gehören sie weg von der Maschine: Protokolle, die den Angreifer überleben.

Wann sich der Aufwand lohnt und wann nicht#

Nicht jeder Dienst verdient eine halbe Stunde Feinarbeit. Die Reihenfolge ergibt sich aus zwei Fragen: Verarbeitet der Dienst Eingaben von außen, und mit welchen Rechten läuft er? Ein von außen erreichbarer Anwendungsprozess ist der erste Kandidat, ein interner Zeitgeber, der eine Datei umbenennt, der letzte.

Bei Diensten aus der Distribution lohnt vorher ein Blick in die mitgelieferte Unit: Viele Pakete bringen bereits einen guten Teil der Einstellungen mit, und was fehlt, ist dann eine Ergänzung statt einer Neuanlage.

# Was steht schon in der Unit des Pakets?
systemctl cat app.service | grep -E 'Protect|Private|Restrict|NoNewPrivileges|Capability'

Der zweite Punkt betrifft die Wartung: Ein Drop-in mit zwanzig Zeilen ist eine Stelle, die bei jedem größeren Versionssprung des Dienstes kurz zu prüfen ist, neue Funktionen brauchen manchmal Pfade oder Systemaufrufe, die vorher nicht nötig waren. Wer das nicht mitzieht, sucht den Fehler später an der falschen Stelle. Ein kurzer Kommentar im Drop-in, warum eine Einschränkung gesetzt wurde, spart genau diese Suche.

MRMedia

Geschrieben aus dem laufenden Betrieb: MRMedia betreibt eigene Hosts, Kundendienste und deren Härtung in der EU. Eine veraltete Anleitung ist hier ein Sicherheitsproblem, kein Schönheitsfehler. Korrekturen bitte über Discord.