Systemhärtung mit systemd

Ein in der Information viel zitierter Spruch1 lautet: “Jedes nicht triviale Programm enthält mindestens einen Fehler.” Wie schwerwiegend dieser eine Fehler und seine Kumpel so sind, lässt sich nicht vorhersagen. Daher tauchen auch in gut abgehangenen Programmen immer wieder schwerwiegende Sicherheitslücken auf. Läuft ein Programm lange und ist es auch noch von außerhalb des ausführenden Rechners erreichbar, entsteht die “interessante” Kombination aus potentiellen Sicherheitslücken und großer Angriffsfläche. Leider trifft diese Beschreibung auf die meisten Dienste (Services) auf einem Serversystem zu.

Hier stellt sich also die Frage: Wie minimiert man die Risiken? Die Antwort ist seit Jahren die gleiche: “Härtung”

Das System sollte so gebaut sein, dass es sich möglichst schwer überlisten lässt. Dabei geht man immer davon aus, dass es eine Schwachstelle gibt und versucht einem potentiellen Angreifer möglichst viele Steine in den Weg zu legen. Dienste als unprivilegierte Benutzer auszuführen ist eine fundamentale Möglichkeit der Härtung. Ist es einem Angreifer möglich das Programm zu übernehmen, hat er hoffentlich nicht genügend Rechte größeren Schaden anzurichten.

Linux bietet noch viele weitere Möglichkeiten einem Angreifer das Leben zu erschweren: chroot, SELinux, Namespaces und Container sind nur einige der Technologien, welche versuchen die Möglichkeiten eines Angreifers einzuschränken. Leider sind viele komplex zu benutzen und haben durchaus Potential einem Administrator den Tag zu verderben.

Auch unaufmerksame Leser werden mittlerweile gemerkt haben, dass ich zu den Fans von systemd gehöre. Auch in Bezug auf die Härtung von Diensten bringt systemd einige spannende Möglichkeiten mit. Keine davon ist wirklich neu. Aber viele waren bisher mit einem Haufen Scripting und Basteln verbunden - wenn man sie überhaupt stabil zum Laufen bekommen hat. Systemd liefert diese Methoden jetzt in einem einfach zu nutzenden Paket. Häufig genügt es eine einzige Option zu setzen, um die Möglichkeiten eines Angreifers empfindlich einzuschränken. Da wäre es schon fast fahrlässig, diese Möglichkeiten nicht zu verwenden.

Alle hier vorgestellten Härtungen beziehen sich auf System-Services. User-Services verhalten sich bei einigen der Optionen anders! Will man einen User-Service härten, ist daher Nachlesen in der Man-Page angesagt.

Basics

Bevor wir uns mit der weiteren Härtung eines systemd-Service befassen, möchte ich erst einmal das Offensichtliche aus dem Weg räumen: User= und Group= gehören in die Konfiguration jedes systemd-Service der von außen erreichbar ist. Die einzige Ausnahme sind hier Dienste, welche die root-Rechte zum Starten benötigen und diese anschließend selbst abgeben. Apache gehört hier beispielsweise dazu, da er seine privaten Schlüssel (für SSL/TLS) als root liest und dann auf die Rechte des http2-Benutzers zurückfällt.

Virtuelle Welten

Fangen wir zuerst einmal mit dem Dateisystem an. Hier liegt viel Potential, denn ein Angreifer braucht erst einmal zwei Dinge auf dem System: Gadgets und Persistenz. Ein Gadget ist ein Baustein, den ein Angreifer zu seinem Vorteil nutzen kann. Das kann ein anderes Programm sein (welches vielleicht noch eine schöne Schwachstelle hat) oder ein Betriebssystemprogramm welches dem Angreifer nützt.

Persistenz ist wichtig, damit der Angreifer das System nicht immer wieder übernehmen muss. Im Idealfall will dieser auch nach einem Reboot noch präsent sein und das Bitcoin-Mining soll ebenfalls weiter laufen. Für das alles bietet das Dateisystem des Servers gute Voraussetzungen. Es ist voll mit Programmen und Dateien die einem nützlich sein können. Oft sind Dateisystemrechte nicht korrekt gesetzt und so findet sich schnell ein passendes Werkzeug oder ein beschreibbares Verzeichnis.

Hier können wir ansetzen und unserem Angreifer einige seiner Möglichkeiten wegnehmen.

Um zu demonstrieren was die einzelnen Optionen in systemd mit dem Dateisystem anstellen, habe ich mir einen minimalen systemd-Service geschrieben. Das Unit-File sieht folgendermaßen aus:

[Unit]
Description=Test Service

[Service]
Type=oneshot
ExecStart=bash -c 'mount|sort; find / -maxdepth 2|sort'
StandardOutput=file:/tmp/myservice.log

Was dieser Service tut ist: Er zeigt die vorhanden Mount-Points und die ersten zwei Ebenen der erreichbaren Verzeichnisse an. Damit lässt sich schön überprüfen, was von dem Dateisystem noch übrig bleibt, sobald die Härtungsoptionen dazu kommen. Der gesamte Output wird sortiert (um ihn besser vergleichen zu können) und in die Datei /tmp/myservice.log geschrieben.

Der Service läuft absichtlich erst einmal als Root, damit deutlich wird, was potentiell alles zur Verfügung steht. Natürlich lässt niemand einen Service als root laufen.

Der Output sieht für die Mountpoints nach einem normalen Start erst einmal so aus:

bpf on /sys/fs/bpf type bpf (rw,nosuid,nodev,noexec,relatime,mode=700)
configfs on /sys/kernel/config type configfs (rw,nosuid,nodev,noexec,relatime)
debugfs on /sys/kernel/debug type debugfs (rw,nosuid,nodev,noexec,relatime)
/dev/mapper/ssd-root on / type ext4 (rw,relatime)
dev on /dev type devtmpfs (rw,nosuid,relatime,size=1943264k,nr_inodes=485816,mode=755)
devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000)
/dev/sda1 on /boot type ext4 (rw,relatime)
fusectl on /sys/fs/fuse/connections type fusectl (rw,nosuid,nodev,noexec,relatime)
gvfsd-fuse on /run/user/1000/gvfs type fuse.gvfsd-fuse (rw,nosuid,nodev,relatime,user_id=1000,group_id=1000)
hugetlbfs on /dev/hugepages type hugetlbfs (rw,relatime,pagesize=2M)
mqueue on /dev/mqueue type mqueue (rw,nosuid,nodev,noexec,relatime)
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
pstore on /sys/fs/pstore type pstore (rw,nosuid,nodev,noexec,relatime)
run on /run type tmpfs (rw,nosuid,nodev,relatime,mode=755)
securityfs on /sys/kernel/security type securityfs (rw,nosuid,nodev,noexec,relatime)
sys on /sys type sysfs (rw,nosuid,nodev,noexec,relatime)
systemd-1 on /proc/sys/fs/binfmt_misc type autofs (rw,relatime,fd=28,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=14503)
tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev)
tmpfs on /run/user/1000 type tmpfs (rw,nosuid,nodev,relatime,size=391536k,mode=700,uid=1000,gid=1000)
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
tmpfs on /tmp type tmpfs (rw,nosuid,nodev)

Bei der Verzeichnishierarchie ist alles dabei, was auf meinem Arch Linux Notebook so zu finden ist. Ich habe den Output etwas gekürzt.

/bin -> /usr/bin
/boot/... 6 Verzeichnisse und Dateien
/dev/... 177 Verzeichnisse und Dateien
/etc/... 168 Verzeichnisse und Dateien
/home/... ein paar Home-Verzeichnisse
/lib -> /usr/lib
/lib64 -> /usr/lib
/lost+found
/mnt
/opt/... 6 Verzeichnisse und Dateien
/proc/... 250 Verzeichnisse und Datein
/root/... etwas Zeugs (z. B. Backups von Konfigurationsdateien)
/run/... 24 Verzeichnisse und Dateien
/sbin -> /usr/bin
/srv
/srv/ftp
/srv/http
/sys
/sys/block
/sys/bus
/sys/class
/sys/dev
/sys/devices
/sys/firmware
/sys/fs
/sys/hypervisor
/sys/kernel
/sys/module
/sys/power
/tmp/... Alle möglichen Temporärdateien
/usr/... 11 Dateien und Verzeichnisse
/var/... 15 Dateien und Verzeichnisse

Wow, das ist wirklich viel Zeug, auf das unser Service hier potentiell Zugriff hat.

ProtectSystem

Die erste Option zur Härtung eines Service ist ProtectSystem. Diese kennt mehrere Werte: true oder yes lassen wir mal außen vor, da sie aus meiner Sicht zu schwach ist und wenig mit Härtung zu tun hat. Wer Näheres wissen möchte kann die Man-Page systemd.exec befragen. Bleiben noch full und strict.

OptionBeschreibung
fullMounted /usr, /boot und /etc als nicht beschreibbar.
strictMounted das gesamte Dateisystem (außer /dev, /proc und /sys) als nicht beschreibbar.

Gerade bei der Verwendung von strict ist Persistenz für unseren Angreifer schon mal ein gutes Stück schwerer geworden. Allerdings gibt es durchaus Services, welche auf das Dateisystem schreiben möchten. Hierfür gibt es die Option ReadWritePaths, welche einzelne Verzeichnisse wieder beschreibbar macht.

Nach dem Setzen der Option ProtectSystem=strict hat sich die Mount-Liste etwas verändert3:

/dev/mapper/ssd-root on /etc type ext4 (ro,relatime)
/dev/mapper/ssd-root on /usr type ext4 (ro,relatime)
/dev/mapper/ssd-root on / type ext4 (ro,relatime)
/dev/sda1 on /boot type ext4 (ro,relatime`)

ProtectHome

Die meisten Dienste brauchen keinen Zugriff auf die Home-Verzeichnisse der auf dem System angelegten Benutzer. Da in den Home-Verzeichnissen oft sensible Daten liegen, ist es sinnvoll unserem potentiellen Angreifer den Zugriff auf /home zu verwehren. Das geht über die Option ProtectHome. Diese kann die Home-Verzeichnisse nur lesbar machen oder komplett ausblenden. Ich würde immer zweiteres empfehlen. Hat der Service ein eigenes Home-Verzeichnis, kann man dieses über BindPaths oder BindReadOnlyPaths wieder sichtbar machen ohne das die anderen Home-Verzeichnisse wieder auftauchen. Das vermeidet auch das Risiko, dass später angelegte Home-Verzeichnisse für den Service versehentlich sichtbar werden.

Nach dem Setzen der Option ProtectHome=tmpfs ist /home in der Dateiliste ein leerer Ordner. Ausgezeichnet.

TemporaryFileSystem

Häufig läuft auf einem Server nicht nur ein Service. Viele Dienste haben ihre Datenverzeichnisse unter /srv. Aber warum sollte der Webserver auch das Datenverzeichnis des Fileservers sehen? Jetzt genügt nur eine einzige unbedachte Berechtigung und ein Angreifer hat vom Webserver aus Zugriff auf unsere internen Daten.

Doch auch das lässt sich mit systemd vermeiden: Die Option TemporaryFileSystem erlaubt es Pfade im Dateisystem durch leere, temporäre Ordner zu ersetzen. In unserm Beispiel sind /srv/ftp und /srv/http sichtbar. Mit TemporaryFileSystem=/srv:ro lässt sich dies verhindern. Ist diese Option aktiv, ist das Verzeichnis /srv für unseren Service leer. Der Suffix :ro sorgt außerdem noch dafür, dass das virtuelle Verzeichnis nicht beschreibbar ist. Somit hat unser Angreifer hier auch keine Möglichkeit eigene Programme abzulegen.

Ein Einwand ist bestimmt: “Aber für den FTP-Server wäre es dumm, wenn er /srv/ftp nicht mehr lesen könnte.” Das Stimmt und daher kann über BindPaths oder BindReadOnlyPaths das Verzeichnis /srv/ftp wieder sichtbar gemacht werden. Muss das Verzeichnis nicht beschreibbar sein, ist BindReadOnlyPaths immer vorzuziehen. Nur wenn auch wirklich Daten geschrieben werden sollen, ist BindPaths nötig.

Das heißt die Konfiguration eines hypothetischen FTP-Servers müsste die folgenden beiden Zeilen im Abschnitt [Service] enthalten:

TemporaryFileSystem=/srv:ro
BindReadOnlyPaths=/srv/ftp

Damit sieht /srv dann so aus:

/srv
/srv/ftp

Das Verzeichnis /srv/http ist in unserem Beispiel verschwunden.

Auch die Verzeichnisse /opt, /boot und /mnt sind nicht für viele Services notwendig. Sie lassen sich ebenso über TemporaryFileSystem leeren.

Auf jedem System gibt es noch ein Dateisystem, welches viele Informationen über einen Server enthält. Ein Zugriff darauf kann ein wahres Fest für einen Angreifer sein. Gerade wenn die Dateisystemrechte verbogen wurden, um unprivilegierten Benutzern Zugriff auf einige Dateien zu erlauben. Na… wer kommt darauf… stimmt, /var. Dort liegen die Log-Dateien und Bewegungsdaten von allen auf dem System laufenden Diensten ab. Also sollte man auch hier großzügig die Option TemporaryFileSystem=/var:ro setzen. Allerdings brauchen viele Dienste doch Zugriff auf einige Verzeichnisse unter /var. Diese lassen sich natürlich wie gewohnt über BindPaths und BindReadOnlyPaths einblenden. Vor allem /var/run sollte man der Unit zugänglich machen. Dort liegen viele Sockets von anderen Diensten, auf welche man evtl. zugreifen möchte. Da /run ebenfalls zugänglich ist und sich die beiden auf modernen Linux-Systemen nicht unterscheiden, wird die Sicherheit hierdurch nicht wirklich geschwächt. Der Kompatibilität kommt es allerdings sehr zugute.

Doch systemd wäre nicht systemd, wenn es beim Ausblenden von Verzeichnissen nicht noch ein paar Extras gäbe:

Units können über die Optionen StateDirectory, CacheDirectory und LogsDirectory angeben, welche Verzeichnisse sie für ihren Zustand, den Cache und die Logs verwenden. Hierbei wird der angegebene Wert mit einem Basispfad kombiniert:

OptionBasispfadPersistent
StateDirectory/var/lib/ja
CacheDirectory/var/cache/ja
LogsDirectory/var/log/ja
RuntimeDirectory/run/nein
ConfigurationDirectory/etc/ja

Die Liste enthält zusätzlich noch die beiden Optionen RuntimeDirectory und ConfigurationDirectory. Sie funktionieren genauso wie die bereits erwähnten Optionen, jedoch unterscheidet sich der Basispfad. Wichtig ist dabei noch, dass das mit RuntimeDirectory angegebene Verzeichnis beim Beenden der Service-Unit geleert wird.

Konkret führt der Eintrag StateDirectory=myunit dazu, dass /var/lib/myunit als Verzeichnis für den Applikationszustand bereitgestellt wird. Außerdem wird es automatisch beschreibbar unterhalb von /var eingeblendet, ohne dass hierfür noch eine gesonderte BindPaths Option nötig wäre. Alle diese Optionen erlauben es auch mehrere Pfade anzugeben. Näheres hierzu enthält wieder die Man-Page von systemd.exec.

PrivateTmp

/tmp ist oft nicht nur der Ablageort für die Temporärdateien von Prozessen, sondern auch der unaufgeräumte Dachboden von Administratoren. Was man mal schnell zwischenspeichern will landet in /tmp. Daher residieren hier oft Dateien mit zweifelhaften Dateisystemrechten und interessanten Informationen. Natürlich ist es eine gute Idee, einem potentiellen Angreifer den Zugriff auf diese Dateien so schwer wie möglich zu machen. Hierfür gibt es die Option PrivateTmp. Wird diese aktiviert, wird ein eigenes tmpfs für den Prozess erzeugt und unter /tmp bereitgestellt. So ist /tmp nach jedem Start des Prozesses sauber und von allen anderen Prozessen und Benutzern auf dem System isoliert.

PrivateTmp=yes sollte also bei keinem Service fehlen.

PrivateDevices

Eine Zeile in der Dateiliste am Anfang dieses Kapitels ist auffällig: /dev/... 177 Verzeichnisse und Dateien Muss unser Webserver wirklich potentiell auf alle Geräte des Servers Zugriff haben? Muss er wissen, was für ein HBA im Server steckt und wie die Festplatten partitioniert sind?

Die Antwort ist bei den meisten Diensten ein klares “nein”. Um dieses “nein” auch durchzusetzen, gibt es die Option PrivateDevices. Ein PrivateDevices=yes erzeugt für den Service ein eigenes /dev Verzeichnis, in welchem nur noch die für den Betrieb eines Linux-Programmes nötigen Geräte vorhanden sind:

/dev
/dev/char
/dev/core
/dev/fd
/dev/full
/dev/hugepages
/dev/log
/dev/mqueue
/dev/null
/dev/ptmx
/dev/pts
/dev/random
/dev/shm
/dev/stderr
/dev/stdin
/dev/stdout
/dev/tty
/dev/urandom
/dev/zero

Das ist schon sehr viel übersichtlicher. Interessanterweise laufen die meisten Dienste auch ohne Zugriff auf 177 Devices.

ProtectKernelTunables

Bleibt noch eine letzte Option die sich auf das Dateisystem bezieht: ProtectKernelTunables

Sie macht /proc/sys, /sys, /proc/sysrq-trigger, /proc/latency_stats, /proc/acpi, /proc/timer_stats, /proc/fs und /proc/irq einfach nicht beschreibbar. Damit kann der Service keine Kernel-Einstellungen mehr ändern. Für einen Webserver oder eine Datenbank ist das hoffentlich auch nicht nötig.

Ende vom Lied

Die Konfiguration des Test-Service sieht nun folgendermaßen aus:

[Unit]
Description=Test Service

[Service]
Type=oneshot
ExecStart=bash -c 'mount|sort; find / -maxdepth 2|sort'
StandardOutput=file:/tmp/myservice.log

ProtectSystem=strict
ProtectHome=tmpfs
TemporaryFileSystem=/srv:ro
TemporaryFileSystem=/var:ro
TemporaryFileSystem=/mnt:ro
TemporaryFileSystem=/opt:ro
TemporaryFileSystem=/boot:ro
BindReadOnlyPaths=/srv/ftp
PrivateTmp=yes
PrivateDevices=yes
ProtectKernelTunables=yes

Damit sind schon einige starke Einschränkungen definiert:

  • Der größte Teil des Dateisystems ist nicht mehr beschreibbar.
  • Die Liste der überhaupt sichtbaren Verzeichnisse ist drastisch geschrumpft.
  • Es sind nur noch die nötigsten Devices verfügbar.
  • Kernel-Einstellungen lassen sich nicht mehr ändern.
/
/bin -> /usr/bin
/boot
/dev/... 19 Verzeichnisse und Dateien
/etc/... 168 Verzeichnisse und Dateien
/home
/lib -> /usr/lib
/lib64 -> /usr/lib
/lost+found
/mnt
/nft
/opt
/proc/... 250 Verzeichnisse und Dateien
/root
/run/... 24 Verzeichnisse und Dateien
/sbin -> /usr/bin
/srv
/srv/ftp
/sys
/sys/block
/sys/bus
/sys/class
/sys/dev
/sys/devices
/sys/firmware
/sys/fs
/sys/hypervisor
/sys/kernel
/sys/module
/sys/power
/tmp
/usr/... 11 Dateien und Verzeichnisse
/var
/var/tmp

Außergewöhnliche Fähigkeiten

Nicht nur die X-Men und root haben außergewöhnliche Fähigkeiten. Für Prozesse in Linux sind Root-Rechte schon lange keine binäre Sache mehr. Über Capabilities können einzelne der Fähigkeiten, welche klassisch dem Root-Benutzer vorbehalten waren, auf Prozesse übertragen werden. Allerdings lassen sich hierdurch auch die Fähigkeiten eines Prozesses einschränken. Systemd bietet hierfür die Optionen CapabilityBoundingSet und NoNewPrivileges.

Das CapabilityBoundingSet sollte für normale Dienste auf einen leeren Wert gesetzt werden. Zusätzlich mit der Option NoNewPrivileges verhindert es zuverlässig, dass ein Prozess seine eigenen Privilegien ändert oder eine der in den vorausgehenden Kapiteln gemachten Einschränkungen umgeht.

Für Web-Dienste kann eine besondere Capability notwendig sein: CAP_NET_BIND_SERVICE Sie erlaubt es einem Prozess einen privilegierten Netzwerkport unter 1024 zu binden. Für eine Applikation, welche einen der Standardports verwenden will, ist diese Capability oft notwendig. Sie kann über CapabilityBoundingSet=CAP_NET_BIND_SERVICE gesetzt werden, während alle anderen Capabilities weiterhin gesperrt bleiben. Näheres zu Capabilities sind auch wieder in deren Man-Page zu finden.

Zu Capabilities wird es nochmal einen gesonderten, Artikel geben. Sie bieten oft die Möglichkeit auf setuid root zu verzichten.

Das auch nicht…

Neben den Capabilities und dem Ausblenden und Schreibschützen von Verzeichnissen bietet systemd noch einige weitere Optionen, welche bei der Härtung eines Systems nicht unbeachtet bleiben dürfen.

ProtectKernelModules

Verhindert das Laden neuer Kernel-Module. Hierfür wird zum einen das CapabilityBoundSet entsprechend modifiziert und zum anderen ein System-Call-Filter aktiviert, welcher das Aufrufen von Funktionen zur Modulverwaltung blockiert. Diese Option kann häufig ohne weitere Nebenwirkungen gesetzt werden, da nur wenige Dienste wirklich Kernel-Module laden müssen.

ProtectKernelLogs

Verhindert das Lesen des Kernel-Logs. Da dieses sehr interessante Informationen über den Systemzustand enthält, ist es fast immer eine gute Idee diese Option zu aktivieren.

ProtectControlGroups

Verhindert Schreibzugriff auf alle Einstellungen der Kernel-Control-Groups (unter /sys/fs/cgroup). Die meisten Dienste (außer Container-Manager) brauchen diesen Zugriff nicht. Systemd verwendet jedoch Control-Groups um einige Einschränkungen für die laufenden Dienste umzusetzen. Daher sollte der Schreibzugriff fast immer mit ProtectControlGroups=yes blockiert werden, damit der Prozess nicht doch noch aus seinem Gefängnis entkommen kann.

Für die ganz Harten

Kommen wir nun zu zwei Optionen welche die Sicherheit nochmals steigern, welche jedoch häufiger zu Nebenwirkungen führen, als die bisher aufgezählten Möglichkeiten: MemoryDenyWriteExecute und SystemCallFilter.

Beide Einstellungen machen es für einen Angreifer nochmals deutlich schwerer, einen Prozess zu übernehmen. Allerdings sollte man sie nicht einfach “mal eben so” aktivieren. Sie können zu merkwürdigem Verhalten von Prozessen führen, welches manchmal erst nach Tagen auffällt 4. Außerdem bedürfen sie eines hohen Pflegeaufwandes, wenn die durch sie geschützte Software sich ändert.

Wer diese Optionen aktiviert sollte also einen guten Testplan und viele automatisierte Tests in der Schublade liegen haben.

MemoryDenyWriteExecute

Verhindert es einen Speicherbereich gleichzeitig als beschreibbar und ausführbar zu markieren. Hierdurch wird es einem Angreifer erschwert Daten über eine Schwachstelle in den Speicher zu kopieren und diese anschließend auszuführen. Allerdings brauchen viele Programme (z. B. JIT-Compiler, dynamische Codeausführung, etc.) genau diese Fähigkeit. Daher ist diese Option mit Vorsicht zu genießen.

SystemCallFilter

Mit dieser Option können einzelne System-Calls verboten werden. Aufgrund der Menge und des durch die Weiterentwicklung von Linux dynamischen Angebots an Systemaufrufen sollte man es sich hier tunlichst verkneifen eigene Listen zu pflegen. Systemd bietet hierfür einige Standardsets an Systemaufrufen an, welche man deaktivieren kann. Die Liste findet sich in der Man-Page von systemd.exec.

Im Notfall - wenn also in einem bestimmten Systemaufruf eine kritische Sicherheitslücke klafft - kann man über diese Option den betroffenen Systemaufruf filtern.

Für den Normalbetrieb bringt systemd ein Set von Systemaufrufen mit, welches normalerweise für alle gängigen Dienste ausreicht:

SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM

Wie die Man-Page schon schreibt, sollte man dieses Set als Ausgangspunkt für weitere Experimente zur Systemaufruffilterung nutzen.

Netzwerk

Um einen Dienst auch netzwerk-seitig weiter einzuschränken sind Netzwerk-Namespaces das Mittel der Wahl. Ein Netzwerk-Namespace stellt quasi einen eigenen Netzwerkstack mit Routing-Tabelle, Firewallregeln und Interfaces für den Prozess bereit. Die Konfiguration ist allerdings alles andere als trivial. Um diesen - sowieso schon recht langen - Artikel nicht komplett ausufern zu lassen, vertage ich das Thema auf ein anders Mal.

Analysewerkzeug

Seit systemd 244 bietet dieser eine Möglichkeit den Härtungszustand des Systems und einzelner Services selbst zu überprüfen. Das Kommando systemd-analyze security liefert eine übersichtliche Liste zum Sicherheitszustand aller systemd-Units:

UNIT                                 EXPOSURE PREDICATE HAPPY
apcupsd.service                           9.6 UNSAFE    😨
postgresql.service                        5.4 MEDIUM    😐
systemd-ask-password-console.service      9.3 UNSAFE    😨
systemd-ask-password-wall.service         9.4 UNSAFE    😨
systemd-coredump@0.service                3.1 OK        🙂
systemd-initctl.service                   9.3 UNSAFE    😨
systemd-journald.service                  4.3 OK        🙂
systemd-logind.service                    2.7 OK        🙂
systemd-networkd.service                  2.7 OK        🙂
systemd-rfkill.service                    9.3 UNSAFE    😨
systemd-timesyncd.service                 2.0 OK        🙂
systemd-udevd.service                     7.0 MEDIUM    😐

Diese Liste mit den Smileys ist eine gute Möglichkeit, sich einen Überblick zu verschaffen. Um mehr zu erfahren, kann man an das systemd-analyze security-Kommando einfach den Namen einer Unit anhängen. Dann gibt das Kommando eine lange Liste mit Härtungsoptionen für diese Unit aus und zeigt an wie diese eingestellt sind. Daneben gibt es eine Wertung, wie kritisch eine nicht gesetzte Option aus Sicht der systemd-Macher ist.

Wichtig ist hierbei, dass das Kommando natürlich nicht weiß, was ein Service tut. Einfach alle Optionen zu aktivieren dürfte in den seltensten Fällen möglich oder sinnvoll sein. Für einen Überblick, über die in einer systemd-Version vorhandenen Härtungsoptionen und als Inspiration für weitere Verbesserungen ist das Kommando allerdings sehr gut zu gebrauchen. Es nimmt einem jedoch nicht ab, selbst zu überlegen, wie viel von den Vorschlägen man umsetzen sollte.

Fazit

Wie man sieht, bringt systemd einen riesigen Satz an Härtungsoptionen mit. Wie immer bei solchen Einstellungen muss der Sicherheitsgewinn gegenüber dem administrativen Aufwand abgewogen werden. Auch der Verbrauch an zusätzlichen Systemressourcen für die ganzen Virtualisierungsschichten ist dabei zu berücksichtigen.

Trotz allem, ist ein gewisses Grundset an Härtungsoptionen für alle von außen erreichbaren Dienste eine gute Idee. Die folgenden Einstellungen kann man als Ausgangspunkt für eigene Experimente verwenden:

[Unit]
Description=Whatever

[Service]
#Type=simple
#ExecStart=

# Never run a service as root
# Only exception: If it drops the privileges by itself (like Apache)
#Group=
#User=

# File system hardening
ProtectSystem=strict
ProtectHome=tmpfs
PrivateTmp=yes
PrivateDevices=yes

# Show/Hide working directories
TemporaryFileSystem=/srv:ro
#BindReadOnlyPaths=/srv/ftp
#BindPaths=/srv/ftp
TemporaryFileSystem=/var:ro
BindPaths=/var/run
TemporaryFileSystem=/opt:ro
TemporaryFileSystem=/mnt:ro
TemporaryFileSystem=/boot:ro

# Provide some directories for this service
#RuntimeDirectory=
#StateDirectory=
#CacheDirectory=
#LogsDirectory=
#ConfigurationDirectory=

# Protect other parts of the system
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes

# System call filter
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM

# Restrict capabilites
CapabilityBoundingSet=
#CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=yes

Und wie immer gilt: Dies ist keine Webseite mit Copy-Paste-Anleitungen. Das Thema ist komplex und es kann eine Menge schief gehen. Daher ist es wichtig jede Zeile Code und Konfiguration zu verstehen, bevor man diese auf Systemen einsetzt, die einem selbst5 wichtig sind.


  1. Zu diesem Spruch war es mir nicht möglich eine Quelle zu finden. Wer unbedingt eine Quelle angeben möchte kann ihn - wie üblich - einfach Albert Einstein unterschieben. ↩︎

  2. oder www oder httpd oder apache↩︎

  3. nachfolgend sind aus Platzgründen nur die geänderten Zeilen dargestellt. ↩︎

  4. Wenn die Änderung schon lange wieder vergessen ist. ↩︎

  5. …oder dem Arbeitgeber… ↩︎