QVINTVS · SCRIBET

Verschlüsselte Root-Partition auf dem Raspberry Pi

Der ungewöhnliche Boot-Prozess der Raspberry Pi macht es nicht ganz leicht, die Root-Partition vollständig zu verschlüsseln.

Der Bootprozess des Raspberry Pi unterscheidet sich von demjenigen eines gewöhnlichen x86-basierten Desktop-Rechners oder Laptops ganz erheblich. Dort läuft der reguläre Bootprozess wie folgt ab:

  1. Einschalten
  2. Power-On Self-Test (POST)
  3. Firmware wird geladen (BIOS oder EFI)
  4. Firmware startet den Bootloder der ersten oder konfigurierten Platte (bei EFI optional)
  5. Bootloader (oder bei EFI das EFI selbst) startet den Kernel
  6. Der Kernel lädt das initramfs von der Partition, auf der er sich befindet
  7. Das initramfs bereitet den Hauptsystemstart vor, insbesondere besteht hier die Möglichkeit, das Passwort für eine verschlüsselte Root-Partition zu erfragen.
  8. Das initramfs bindet die per Kernel-Parameter root= übergebene Partition in das Verzeichnis /new_root ein und führt das Programm switch_root aus, welches von Busybox mitgeliefert wird und welches ein Chroot in dieses Verzeichnis gefolgt von einem exec /init ausführt.
  9. /init, meist ein Symlink auf /sbin/init, /usr/bin/systemd oder ähnliches, kümmert sich um den Rest des Starts bis zum Login-Prompt.

Beim Raspberry Pi dagegen läuft der Bootprozess so ab:

  1. Einschalten
  2. Firware wird geladen
  3. Firmware liest die Datei config.txt von der ersten Partition der SD-Karte.
  4. Firmware startet den auf der ersten Partition der SD-Karte befindlichen Kernel, und übergibt ihm den Inhalt der cmdline.txt-Datei, die sich auch auf der ersten Partition der SD-Karte befindet.
  5. Der Kernel bindet die Root-Partition ein, welche ihm per Kernel-Parameter root= übergeben wurde.
  6. Der Kernel führt das /init-Programm aus, etwa ein Symlink auf /usr/bin/systemd, das sich um den Rest des Starts bis zum Login-Prompt kümmert.

Insbesondere fällt der Unterschied auf, dass der Raspberry Pi ohne ein Initramfs bootet. Für einen normalen Betrieb ist dies unerheblich, da die meisten Anwendungszwecke der initialen Ramdisk mittlerweile entfallen sind, etwa das Anlegen der Blockdateien unter /dev, welches heute von udev bzw. systemd automatisiert übernommen wird. Jedoch gibt es auch heute noch Fälle, in denen ein initramfs erforderlich ist, insbesondere die Verwendung einer verschlüsselten Root-Partition, da der Kernel selbst nicht in der Lage ist, ein Kennwort dafür anzufordern (warum eigentlich nicht?), sodass Hilfsprogramme wie cryptsetup(8) erforderlich werden. Diese müssen vor Einbindung der verschlüsselten Root-Partition zur Verfügung stehen, können sich demzufolge auf dieser nicht befinden. Hierfür bietet das initramfs sich an. Nur: Der Raspberry Pi hat keins.

Es ist aber offenkundig möglich, eins einzurichten. Forscht man weiter nach, stößt man in der offiziellen config.txt-Dokumentation für den Raspberry-Pi-Bootloader auf einen Parameter namens initramfs, der eine Datei von der ersten Partition der SD-Karte lesen und an eine angegebene Adresse im Speicher kopieren kann (urks). Diese Speicheradresse wiederum kann man dann dem Kernel über den Parameter initrd= mitgeben, der das dort vorgefundene komprimierte initramfs dann entpackt und als Verzeichnis / einbindet, um unmittelbar darauf das Programm /init mit PID 1 auszuführen.

Unter ArchLinux wird ein initramfs normalerweise mithilfe des Programms mkinitcpio erzeugt, welches automatisch (per Post-Install-Skript) nach jedem Kernel-Update ausgeführt wird und welches dann die Datei /boot/initramfs-linux.img erzeugt. Unter ArchLinuxARM ist dieses Programm weder installiert, noch wird es nach jedem Kernel-Update automatisch ausgeführt. Es befindet sich jedoch in den Repositories.

Die Installation von ArchLinuxARM auf einem RaspberryPi mit verschlüsselter Root-Partition läuft aufgrund der dargelegten Tatsachen abweichend von der Standardprozedur ab. Eine besondere Hürde stellt die Tatsache da, dass die armv6h-Binaries nicht bzw. nur mit einem erheblichen Aufwand (QEMU) auf einem x86-System ausgeführt werden können. Um diese Problematik zu umgehen, wird ArchLinuxARM im Folgenden gewissermaßen „zweimal“ installiert, einmal auf einem externen USB-Stick, und einmal auf der SD-Karte, wobei letztere die verschlüsselte Root-Partition enthalten soll.

Zunächst einmal ist freilich der aktuelle Root-Tarball herunterzuladen. Währenddessen können sowohl der USB-Stick als auch die SD-Karte an einen vorhanden Rechner angeschlossen werden, um danach auf ihre neue Aufgabe vorbereitet werden zu können. Bei /dev/sde handelt es sich bei mir um die SD-Karte, /dev/sdf ist der USB-Stick. Diese Angaben müssen selbstverständlich an die lokalen Abweichungen angepasst werden.

Als erstes werden ein paar Einhängepunkte erstellt:

# cd /mnt
# mkdir boot root root2

Dann wird die Partitionierung vorgenommen, d.h. die erste Partition auf der SD-Karte als FAT32 (wie die RPi-Firmware es erfordert), der Rest als verschlüsselte LUKS-Partition eingerichtet, in der sich wiederum eine einzelne ext4-Partition verbirgt. Der USB-Stick wird vollständig mit einer ext4-Partition bedeckt, er stellt nur das „Hifssystem“ da, welches wir benötigen, um mkinitcpio auf dem Pi ausführen zu können. Er steht nach Abschluss der Arbeiten wieder für andere Zwecke zur Verfügung. Bei der Wahl der Passphrase für die LUKS-Partition sind die üblichen Sicherheitsanforderungen zu beachten.

# fdisk /dev/sde
> o        # Neue Partitionstabelle
> n        # Neue Partition
> p        # Primäre Partition
> 1        # Erste Partition
> [Enter]  # Ersten Sektor akzeptieren
> +100M    # 100 MiB große Partition
> t        # Typ festlegen
> c        # Fat mit LBA
> n        # Neue Partition
> 2        # Zweite Partition
> [Enter]  # Ersten Sektor akzeptieren
> [Enter]  # Letzten Sektor akzeptieren
> w        # Ausführen
# fdisk /dev/sdf
> o       # Neue Partitionstabelle
> n       # Neue Partition
> p       # Primäre Partition
> 1       # Erste Partition
> [Enter] # Ersten Sektor akzeptieren
> [Enter] # Letzten Sektor akzeptieren
> w       # Ausführen
# mkfs.vfat /dev/sde1
# cryptsetup luksFormat /dev/sde2
> geheimepassphrase
# cryptsetup luksOpen /dev/sde2 sdcard
# mkfs.ext4 /dev/mapper/sdcard
# mkfs.ext4 /dev/sdf1

Die neu erstellten Dateisysteme einbinden:

# mount /dev/sde1 /mnt/boot
# mount /dev/mapper/sdcard /mnt/root
# mount /dev/sdf1 /mnt/root2

Nun wird der Root-Dateisystemtarball zweimal entpackt. Einmal für das verschlüsselte Hauptsystem, und einmal für das unverschlüsselte Hilfssystem. Außerdem wird der Inhalt des Verzeichnisses boot/ der offiziellen Anleitung entsprechend auf die erste Partition der SD-Karte kopiert.

# cd /mnt
# tar -xpf /home/quintus/Downloads/ArchlinuxARM-rpi-latest.tar.gz -C root
# tar -xpf /home/quintus/Downloads/ArchlinuxARM-rpi-latest.tar.gz -C root2
# mv root/boot/* boot/

Da das verschlüsselte System zum jetzigen Zeitpunkt nicht bootfähig ist (kein initramfs zum Abfragen der Passphrase), muss zunächst das Hilfssystem auf dem USB-Stick als Root-Partition angegeben werden. Dazu wird die Datei boot/cmdline.txt geöffnet und der Kernel-Parameter root= wie folgt geändert:

root=/dev/sda1

Alle anderen Parameter bleiben unverändert erhalten. Da es sich bei dem USB-Stick um das einzige Blockdevice im engeren Sinne handelt, wird er zwangsweise unter der Bezeichnung sda von udev erkannt werden.

Jetzt können alle Partitionen ausgehangen werden:

# umount boot root root2
# cryptsetup luksClose sdcard

Nachdem diese Kommandos durchgelaufen sind, können SD-Karte und USB-Stick vom bisherigen Rechner getrennt und an den Raspberry Pi angesteckt werden. Außerdem werden eine USB-Tastatur sowie ein Monitor (HDMI) und ein Ethernet-Kabel an den Raspberry Pi angeschlossen. Dann wird das Gerät durch Einstecken des Stromkabels gestartet. Läuft alles gut, befindet man sich jetzt im unverschlüsselten Hilfssystem, auf dem man sich als root anmelden kann (Passwort standardmäßig root). Dort umgehend die Paketdatenbank aktualisieren und mkinitcpio installieren (eine Internetverbindung via DHCP stellt ArchLinuxARM bereits standardmäßig automatisch her, ein manueller Aufruf von dhcpcd entfällt daher):

# pacman -Sy mkinitcpio

Nun wird die Konfigurationsdatei von mkinitcpio, /etc/mkinitcpio.conf so angepasst, dass die HOOKS-Zeile so aussieht (als Editor steht nano zur Verfügung):

HOOKS="base udev autodetect modconf block keyboard encrypt filesystems fsck"

Dabei handelt es sich um die Standard-Hooks von ArchLinux, mit der Besonderheit, dass der filesystems-Hook hinter dem encrypt-Hook ausgeführt wird (da selbige sich in einer verschlüsselten Partition befinden) und dass keyboard vor dem neu einzufügenden encrypt-Hook zu listen ist, um bei der Passworteingabe bereits Tastaturzugriff zu genießen.

Anschließend wird mkinitcpio aufgerufen, um das initramfs zu bauen:

# mkinitcpio -k $(uname -r) -g /boot/initrd

Dies erzeugt die Datei /boot/initrd. Der Name ist frei wählbar und hat keine spezifische Bedeutung. Dieser Befehl muss nach jedem Kernel-Update wie schon erwähnt neu ausgeführt werden, da er Kernelmodule in das initramfs verpackt, und Kernelmodule sind per definitionem abhängig von der Kernelversion.

Mit dem Wissen um die Option initramfs in der Raspberry-Pi-Konfiguration wird nun die Datei /boot/config.txt um folgende Zeile ergänzt:

initramfs initrd 0x00f00000

Dies führt dazu, dass die Datei initrd auf der ersten Partition der SD-Karte vom RPi-Bootloader an die Speicheradresse 0x00f00000 kopiert wird, bevor der Kernel geladen wird. Diese Adresse stammt von dieser Seite, ihre Herkunft ist mir unbekannt. Die Elinux.org-Seite empfiehlt als Adresse dagegen 0x00800000. Die genaue Wahl dürfte jedoch recht unerheblich sein, da es sich nicht um einen fest definierten Wert handelt. Vielmehr wird diese Adresse nun dem Kernel über die Kommandozeilenoption initrd= mitgeteilt, indem die Datei /boot/cmdline.txt entsprechend bearbeitet wird. Neben dieser Option werden die üblichen, für eine verschlüsselte Root-Partition erforderlichen Parameter cryptdevice= und root= hinzugefügt bzw. angepasst:

cryptdevice=/dev/mmcblk0p2:root root=/dev/mapper/root initrd=0x00f00000

Die übrigen Kernel-Parameter bleiben unverändert. cryptdevice= instruiert den encrypt-Hook des initramfs, nach einem Passwort für diese Partition zu fragen und anschließend das entschlüsselte Device-Node mit dem Namen root (das ist der Teil hinter dem Doppelpunkt, der Name ist frei wählbar) unterhalb von /dev/mapper zu erstellen. Dieser Pfad wird dann an die bekannte Kernel-Option root= gegeben, die für das initiale Einhängen von / verantwortlich ist. initrd= dient wie beschrieben dazu, dem Kernel mitzuteilen, wo im Speicher sich das initramfs befindet (dort, wo dem RPi-Bootloader gesagt wurde, dass er es hinpacken soll).

Zuletzt wird die Datei /etc/fstab so abgeändert, dass sie einen neuen Eintrag für das Root-Dateisystem erhält. Folgende Zeile ist als erste in die Datei aufzunehmen, die übrigen Zeilen (es sollte sich lediglich um eine weitere für /boot handeln) rutschen nach unten.

/dev/mapper/root    /    ext4    defaults    0    1

Leerzeile am Ende der Datei nicht vergessen.

Damit sind die Arbeiten abgeschlossen. Neustart:

# systemctl reboot

Das initramfs sollte nunmehr geladen werden und nach einem Passwort für die verschlüsselte Partition fragen. Nach Entschlüsselung wird diese als / einghangen und das Hauptsystem gebootet. Dort nun ebenfalls mkinitcpio installieren:

# pacman -Sy mkinitcpio

Fertig. Bei jedem Kernel-Update muss nun nur noch daran gedacht werden, mkinitcpio mit der Version des zukünftigen Kernels auszuführen (hat man also etwa 3.15.1 laufen und 3.15.2 kam per Update rein, muss der Befehl auf mkinitcpio -k 3.15.2 -g /boot/initrd lauten).

Der RaspberryPi kann nun heruntergefahren werden und der USB-Stick abgezogen werden.

Valete.