dm-crypt, Dienste und ein eigenes Runlevel

Auf meinem Homeserver gibt es eine Datenpartition. Diese ist - falls das Ding einmal geklaut wird - per dm-crypt verschlüsselt. Die Daten des Fileservers und der syncthing Installation liegen auf der Datenpartition. Andere Dienste, wie der Videorekorder (VDR) oder die Telefonanlage sind nicht verschlüsselt abgelegt. Daher ist es natürlich keine Option beim Starten auf die Eingabe des Kennwortes zu warten. Sollte die USV den Server doch einmal herunterfahren, wäre sonst bis zur Eingabe des Kennwortes kein Telefonieren oder Fernsehen mehr möglich.

Bisher wurde die verschlüsselte Partition über ein Script eingehängt. Anschließend wurden die Dienste gestartet, welche zugriff auf die verschlüsselten Daten benötigen. Das funktioniert zwar, allerdings hat diese Lösung auch ihre Probleme:

  • Die Dienste lassen sich manuell starten, auch wenn die Datenpartition nicht eingehängt ist. Dann schreiben einige Dienste unsinniges Zeug in den Mountpoint.
  • Die Dienste können weiterhin aktiviert werden. Dann versuchen sie beim Systemstart zu starten und finden einen leeren Mountpoint vor.
  • Um das Datenvolume auszuhängen muss man manuell alle Dienste stoppen die es verwenden. Das Script kann das zwar auch erledigen, allerdings ist das Fehlerhandling sehr aufwändig und vor dem Aushängen muss man sicher gehen, dass wirklich alle Dienste gestoppt sind.

Konfigurieren statt Programmieren

Als Alternative bietet sich systemd an. Anstelle ein kompliziertes Script zu erstellen, genügen etwas Knowhow und einige Konfigurationsdateien. Diese erledigen den Job besser als das Script und beseitigen nebenbei noch die aufgeführten Probleme.

Der Schlüsselbaustein hierfür ist die systemd-Direktiven BindsTo. Sie sagt aus, dass die angegebene Unit nur in Verbindung mit einer bestimmten anderen Unit funktionsfähig ist. Das geht so weit, dass die Unit beendet wird, wenn die angegebene Unit aus irgend einem Grund inaktiv wird. Nehmen wir also an, dass unser Datenverzeichnis unter /mnt/data eingehängt wird. Dann wäre die zuständige systemd-Unit mnt-data.mount. Das lässt sich leicht über den Befehl systemctl list-units --type=mount verifizieren:

UNIT                          LOAD   ACTIVE SUB     DESCRIPTION
-.mount                       loaded active mounted /
boot.mount                    loaded active mounted /boot
dev-hugepages.mount           loaded active mounted Huge Pages File System
dev-mqueue.mount              loaded active mounted POSIX Message Queue File System
mnt-data.mount                loaded active mounted /mnt/data
proc-sys-fs-binfmt_misc.mount loaded active mounted Arbitrary Executable File Formats File System
run-user-0.mount              loaded active mounted /run/user/0

Für Dienste, die man selbst unter /etc/systemd angelegt hat genügt es also einfach die folgenden Zeilen zu ergänzen:

[Unit]
BindsTo=mnt-data.mount
After=mnt-data.mount

[Install]
WantedBy=data.target

Die After direktive ist wichtig, da BindsTo noch keine Aussage über die Reihenfolge der Aktivierung trifft. Damit der Dienst also nicht gleich mit einem Fehler aussteigt, sollte er besser nach dem Mounten der Datenpartition aktiviert werden.

Diese Änderung sorgt schon einmal dafür, dass die Dienste nur starten, wenn das Datenverzeichnis eingehängt ist. Aber das ist nur die halbe Miete.

Den [Install]-Abschnitt ignorieren wir für einen Moment.

Fremdgesteuert

Bevor ich darauf eingehe, wie man dafür sorgt, dass das Verzeichnis auch wirklich eingehängt wird, möchte ich noch schnell erklären wie man Unit-Dateien anpasst, die durch ein Softwarepaket unter /lib/systemd installiert oder dynamisch unter /run/systemd erstellt wurden. Die ersten kann man natürlich auch direkt anpassen, aber dass ist weder guter Stil noch besonders robust1. Aber systemd bietet auch hier eine Lösung. Unter /etc/systemd/system/ erstellt man einfach ein Verzeichnis, dass so heißt wie die Unit, deren Konfiguration man anpassen möchte und hängt ein .d an. Soll also die Konfiguration der Unit smbd geändert werden, erstellt man einfach das Verzeichnis /etc/systemd/system/smbd.d. Alle in diesem Verzeichnis liegenden Dateien werden nach der eigentlichen Unit-Konfiguration geladen und können Werte in dieser überschreiben.

Um also die im vorausgehenden Kapitel dargestellte Abhängigkeit zur Unit smbd hinzuzufügen, wird einfach die Datei /etc/systemd/system/smbd.d/datadependency.conf mit folgenden Inhalt erstellt:

[Unit]
BindsTo=mnt-data.mount
After=mnt-data.mount

[Install]
WantedBy=data.target

Genau: Das ist der gleiche Text wie im letzten Kapitel. Er wird von systemd einfach zur vorhandenen Unit aus /lib/systemd/system hinzugefügt ohne dass die Datei geändert werden muss. So bleibt diese Ergänzung auch nach dem Update eines Paketes, oder wenn die Unit dynamisch neu erzeugt wird, bestehen.

Mountpoint

Doch wo kommt eigentlich die Unit mnt-data.mount her? Muss man diese manuell konfigurieren? Nein, natürlich nicht. Die man-Page zu systemd.mount gibt dazu Auskunft:

Mount units may either be configured via unit files, or via /etc/fstab. Mounts listed in /etc/fstab will be converted into native units dynamically at boot and when the configuration of the system manager is reloaded. In general, configuring mount points through /etc/fstab is the preferred approach.

Aha, die mount-Units werden automatisch aus der Datei /etc/fstab erzeugt, die jedem Linux-Benutzer wohl bekannt sein dürfte. Sie enthält die Konfiguration für alle Dateisysteme, welche vom Betriebssystem angebunden werden sollen. Dort ist auch der Mountpoint /mnt/data eingetragen:

LABEL=data /mnt/data xfs  nodev,nosuid,pquota,noauto 0 0

Die Namen der Mount-Units werden durch systemd bestimmt. Der Name wird vom in /etc/fstab eingetragene Mount-Point abgeleitet. Welcher Name für einen bestimmten Mount-Point ermittelt wird, kann über das Tool systemd-escape angezeigt werden. Beim Aufruf von systemd-escape -p "/mnt/data"2 erhält man die Ausgabe mnt-data und dies ist auch der Name der Mount-Unit. Natürlich kann man auch einfach unter /run/systemd/generator nachsehen welche Mount-Units erstellt wurden und die korrekte heraussuchen.

Kettenbildung

Nun sind also die nötigen Dienste vom Mountpoint abhängig. Da die Abhängigkeit über BindsTo erstellt wurde, wird der Dienst sogar wieder gestoppt, wenn der Mountpoint ausgehängt wird. Jetzt fehlt noch eine Möglichkeit, das Crypto-Device automatisch einzuhängen, wenn der Mountpoint eingehängt wird. Hierfür muss das Tool cryptsetup mit den richtigen Parametern aufgerufen und das Kennwort für das Volume eingegeben werden. Das soll natürlich auch automatisch passieren. Also ist ein Script notwendig, welches das verschlüsselte Volume entsperrt:

#!/bin/bash
echo 'mypassword'|/sbin/cryptsetup open --type=luks /dev/sda3 data

Diese Scriptdatei kann unter /usr/local/bin/mount-data.sh abgelegt werden. Um es auszuführen wird noch eine passende systemd-Unit benötigt. Diese Unit wird in der Datei /etc/systemd/system/mnt-data.service definiert:

[Unit]
Description=Unlock the encrypted data volume.

[Service]
Type=oneshot
ExecStart=/usr/local/bin/mount-data.sh
ExecStop=/sbin/cryptsetup close data
RemainAfterExit=yes

[Install]
WantedBy=data.target

Auch hier ignorieren wir für einen Moment den Eintrag im [Install]-Abschnitt.

Diese system-Unit startet das Mount-Script wenn sie gestartet wird und führt cryptsetup mit den Parametern close data aus wenn der Service wieder gestoppt wird.

Leider ist das Script nicht wirklich gut um ein verschlüsseltes Laufwerk einzuhängen. Das Kennwort ist direkt im Script angegeben. Das ist weder sicher, noch sinnvoll. Zum Glück bietet systemd hierfür eine Lösung. Das Kommando systemd-ask-password erlaubt es einem Dienst, ein Kennwort abzufragen. Dies geschieht automatisch über eine interaktive Sitzung oder die lokale Konsole. Worüber die Abfrage erfolgt bestimmt systemd automatisch. Das Script muss also folgendermaßen geändert werden, damit beim Starten des Service das Kennwort für das verschlüsselte Laufwerk abgefragt wird:

#!/bin/bash
/bin/systemd-ask-password --id="cryptsetup:/dev/sda3" "Password for DATA:"|/sbin/cryptsetup open --type=luks /dev/sda3 data

Details zu systemd-ask-password liefert die man-Page. Hier nur so viel: Der --id-Parameter legt einen eindeutigen Identifizierer für die Kennwortabfrage fest und erlaubt es den beteiligten Eingabeagenten die Anfrage zu identifizieren. Als unbenannter Parameter wird die Eingabeaufforderung für die Kennworteingabe übergeben.

Über dieses Script lässt sich nun das verschlüsselte Volume bereitstellen. Das Mounten erfolgt über das Dateisystemlabel. Hier kann natürlich auch der über den Device-Mapper bereitgestellte Pfad in der Datei /etc/fstab eingetragen werden.

Was jetzt noch fehlt ist, dass die Mount-Unit auch von der Unit mnt-data.service abhängig ist. Da mnt-data.mount durch systemd automatisch erstellt wird, muss hierfür der vorausgehend beschriebene Erweiterungsmechanismus genutzt werden. Im Verzeichnis /etc/systemd/system/mnt-data.mount.d wird daher die Datei cryptfs.conf mit folgendem Inhalt angelegt:

[Unit]
# Nötig da die Daten auf der verschlüsselten Partition liegen
Requires=mnt-data.service
After=mnt-data.service

Diese legt fest, dass die Mount-Unit mnt-data.mount von mnt-data.service abhängig ist und dieser zuerst gestartet werden muss. Das Starten der Mount-Unit startet den Service und dieser löst automatisch die Kennwortabfrage aus. Anschließend kann das Volume eingehängt werden.

1up

Die Abhängigkeiten sind nun alle erstellt. Die Dienste, welche auf die Datenpartition zugreifen, werden erst gestartet, wenn die Datenpartition verfügbar ist. Nun könnte man alle Dienste und den mnt-data.service aktivieren (enable) und beim Systemstart würde eine Kennwortabfrage erscheinen, welche das verschlüsselte Volume einhängt. Das Problem dabei ist: Bei einem Server steht man selten vor dem System wenn es neu bootet. Viel besser wäre es, wenn das verschlüsselte Volume erst nach einer entsprechenden Aufforderung eingehängt würde.

Unter System-V-Init gab’ es die Runlevel 0-6 welche unterschiedliche Systemzustände repräsentierten. Jedes Runlevel startete bestimmte Dienste und stoppte andere. Hierdurch konnte ein System erst einmal im Single-User-Modus (Runlevel 2) hochgefahren werden, bevor der Multiuser-Modus (Runlevel 3) aktiviert wurde. Auch unter systemd gibt es etwas Ähnliches wie Targets. Allerdings sind diese benannt und es können mehr als 7 sein. Was liegt also näher, als ein eigenes Runlevel für den Betrieb des Servers mit eingehängter Data-Partition einzurichten. Dieses Runlevel sorgt anschließend dafür, dass zuerst das Volume entschlüsselt wird und dann alle nötigen Dienste starten.

Ich habe das Target data.target getauft. Für dieses Target ist die Datei data.target und das Verzeichnis data.target.wants unter /etc/systemd/system anzulegen.

Die Datei data.target beschreibt das Target genauer:

[Unit]
Description=Multi-User Mode + crypted data directory
Requires=graphical.target
After=graphical.target
Conflicts=rescue.target
AllowIsolate=yes

BindsTo=mnt-data.mount
After=mnt-data.mount

Unter debian ist das Standard-Target das graphical.target. Natürlich kann auch - je nach Distribution - ein anderes Target das Standard-Target sein. Daher kann man über systemctl status default.target das Standard-Target ausgeben. In der Loaded-Zeile steht der Name der tatsächlich geladenen Target-Datei. Da wir, wenn das Target aktiviert wird, nicht die anderen Dienste des Standard-Targets abschalten wollen, müssen wir eine Abhängigkeit zu diesem Eintragen. Hierfür sind die Requires und die After Zeile notwendig. Der Eintrag Conflicts legt fest, dass das Rettungssystem (rescue) ohne das verschlüsselte Laufwerk auskommen muss. Wichtig ist dann noch der Eintrag AllowIsolate, damit das Target auch über systemctl isolate wie ein Runlevel gestartet werden kann.

Jetzt fehlt noch die Abhängigkeit zur mnt-data.mount Mount-Unit damit die anderen Dienste dieses Targets nicht gestartet werden, bevor die Verschlüsselung initialisiert wurde. Hierfür ist nochmal eine BindsTo und eine After Direktive notwendig. Wie bereits unter “Kettenbildung” beschrieben genügt es die Mount-Unit zu starten um das Volume einzuhängen. Da die Mount-Unit vom mnt-data.service abhängig ist, erscheint erst die Kennwortabfrage und dann wird das Data-Volume eingehängt.

Das Verzeichnis data.target.wants enthält, wie bei den durch das System definierten Targets, Symlinks auf die Units, welche durch dieses Target gestartet werden müssen. Und jetzt erklären sich auch die Einträge im [Install]-Abschnitt der einzelnen Units. Diese geben an, dass die Unit, wenn sie mit systemctl enable aktiviert wird, für das Target data.target eingetragen werden soll. Dies geschieht den Eintrag WantedBy=data.target der festlegt, dass dieser Dienst bei seiner Aktivierung dem data-Target zugeordnet werden soll.

Orchesterprobe

Nun können alle Dienste, welche das Data-Volume nutzen, über systemctl enable aktiviert werden. Auch das data.target muss noch über systemctl enable data.target aktiviert werden. Das ist wichtig, damit der symbolische Link auf das Standard-Target angelegt wird.

Nachdem wir nochmal geprüft haben, dass alle Dienste, der Mountpoint und das Standard-Target im Verzeichnis /etc/systemd/system/data.target.wants verlinkt sind, können wir die neue Konfiguration testen. Wenn alles korrekt eingerichtet ist, sollte ein systemctl isolate data.target die Kennwortabfrage für das Data-Volume öffnen, dieses anschließend Mounten und alle abhängigen Dienste starten.

  1. Die Datei würde beim nächsten Paketupdate überschrieben und die Änderungen wären weg. 

  2. Der Parameter -p teilt systemd-escape mit, dass ein Pfad umgewandelt werden soll. Hierdurch wird der erste Slash des Pfades gesondert behandelt. Details verrät auch hier die systemd Dokumentation.