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.