Verschlüsselte Festplatten schützen Daten auch im Fall von Diebstahl. Selbst wenn ganze Racks aus dem Serverraum getragen werden: Ohne das richtige Passwort für die Disk Encryption ist mit den Festplatten nicht viel anzufangen. Also verschlüsseln wir sie. So weit, so selbstverständlich.

Diese Sicherheit hat aber einen Preis. Jede menschliche Intervention beim Neustart kostet Zeit und erhöht die Fehleranfälligkeit: Wird der Boot-Prozess unterbrochen, sei es durch Ablenkung oder einen Zwischenfall, bleibt der Server am Password-Prompt hängen und fährt nicht wieder hoch.

Bei uns kommt es vor, dass wir Server häufiger rebooten müssen. Viel Software lässt sich zwar im laufenden Betrieb updaten oder umkonfigurieren, aber spätestens beim Linux-Kernel geht das nicht, da ist ein Reboot nötig. Deshalb wollten wir eine Lösung finden, mit der wir Server rebooten können, ohne dass ein manueller Schritt zum Entschlüsseln notwendig ist.

Natürlich könnte man versuchen, unseren bisherigen Prozess zu automatisieren, wobei ein anderer Computer mit Zugang zur Passphrase sich per SSH mit dem rebootenden Server verbindet und entschlüsselt. Diese Art von Automatisierung ist aber tückisch, denn sie macht das ganze zu einem verteilten System, mit all den Problemen, die dadurch entstehen. Wir wollten eine Lösung, die nicht von einem weiteren Computer abhängig ist, aber die Sicherheit unserer bestehenden Lösung beibehält. Genau das haben wir in NixOS basierend auf kexec gelöst.

Schnellere Reboots mit kexec

Mithilfe dieses Mechanismus führt der Kernel (k) einen anderen Kernel aus (exec). Der laufende Kernel ersetzt sich also selbst durch einen neuen, ohne dass dabei der eigentliche Server (also die Hardware) runter- und wieder hochgefahren wird. Lediglich das Betriebssystem startet neu. Das Spannende dabei: Der alte Kernel kann im Arbeitsspeicher Informationen bereitlegen, die der neue Kernel benutzen kann. Zum Beispiel eine Passphrase zum Entschlüsseln der Festplatte. Als netten Nebeneffekt überspringen wir die Firmware- und Bootloader-Teile des Reboots, was gerade bei Servern einige Minuten Zeit spart.

Passphrases sicher übergeben

Die LUKS-Passphrase könnte man auch übergeben, indem man sie vor dem Reboot einfach in eine Datei schreibt. Aber: Bei einem vollständigen Reboot fährt sich das System komplett herunter. Damit der neu startende Kernel diese Datei dann lesen könnte, müsste sie also unverschlüsselt gespeichert werden, womit wir aber die Sicherheit unserer Festplattenverschlüsselung ausgehebelt hätten. Mit kexec hingegen lassen sich die Daten über den flüchtigen Arbeitsspeicher des Servers übergeben. Das ist etwas kniffelig – wie wir das umgesetzt haben, zeigen wir euch in diesem Artikel.

Sicherheit und Komfort gleichzeitig

You can’t have your cake and eat it, too? Diese vermeintliche Weisheit wollten wir für den Reboot unserer Server nicht akzeptieren. Wir wollen die Sicherheit von Full Disk Encryption, klar, aber wir wollen auch Komfort, Schnelligkeit und zuverlässige Reboots. Und wenn wir eins nicht tun, dann ist es schnell aufgeben, nur weil eine Lösung auf den ersten Blick nicht funktioniert.

Also haben wir uns auf die Suche gemacht und dabei zwei spannende Quellen mit unterschiedlichen Ansätzen gefunden:

Beide Ansätze haben Nachteile und Vorteile, keiner davon war uns genug. Wir haben deshalb das Beste aus den beiden Ansätzen herausgepickt und daraus unsere eigene Lösung gebaut.

Ansatz Nr. 1: Einmal-Passphrases

Wir generieren eine temporär gültige Passphrase. Aber nicht nur das: In LUKS kann man mehrere Keyslots hinterlegen. Wir erstellen also auch einen Keyslot, geben ihm diese temporäre Passphrase mit, und löschen den Keyslot nach dem erfolgreichen kexec direkt wieder. Dadurch bleibt unsere Verschlüsselung selbst dann sicher, wenn die temporäre Passphrase aus irgendeinem Grund geleakt würde.

Ansatz Nr. 2: Passphrase nicht auf der Commandline übergeben

Auch wenn die Passphrase direkt beim nächsten Boot invalidiert wird, wollen wir extra sichergehen, dass niemand sie aus der Kernel-Commandline auslesen kann, auch nicht in der kurzen Zeit, die wir benötigen, um die Einmal-Passphrase zu invalidieren. Daher nutzen wir die Technik aus dem oben genannten Blogartikel: Die Einmal-Passphrase wird hierbei in ein spezielles initramdisk-Image eingebettet, das extra direkt vor dem kexec-Reboot erstellt wird.

Unsere Lösung für passwortlose Reboots

Und das ist unsere eigene Lösung ganz konkret: Wir erweitern systemd.services."prepare-kexec" um die folgenden Schritte:

  • Wir erstellen eine temporäre LUKS-Passphrase in einem zusätzlichen Key Slot.
  • Wir erstellen ein neues initrd image und ergänzen eine Datei mit dem Key (im RAM).
  • Wir konfigurieren per Kernel-Commandline die Nutzung dieser KeyFile und ermöglichen einen Fallback auf die manuelle Passwort-Eingabe.

Direkt nach dem Mounten des root-Dateisystems entfernen wir den Key Slot per eigener systemd unit in: boot.initrd.systemd.services.

Unser unterbrechungsfreier 2-Minuten-Reboot heute

Heute braucht unser Reboot keine manuellen Eingriffe mehr. Und er dauert auch nur noch etwas mehr als 2 Minuten. Es genügt ein einfaches systemctl start kexec.target. Egal ob manuell oder automatisch ausgelöst – der Server kommt voll funktionsfähig wieder hoch und ist einsatzbereit.

Passwortlose Reboots unter NixOS to go

Du möchtest unsere komplette Implementierung als fertigen Code? Unseren Code findest du in einer Beispielkonfiguration unter diesem Artikel.

Viel Spaß!

Beispiel-NixOS-Modul:

{ config, lib, pkgs, ...}:
let
  luksDevice = config.boot.initrd.luks.devices."yourdevice".device;
in
{
  # Concept:
  # To reboot a server using kexec, we need to alter the nixos
  # prepare-kexec script, because we use full disk encryption.
  # We add a temporary, random key to LUKS keyslot 31. This key is
  # also added to the init ram disk image.
  # We have to set the keyfile and fallbackToPassword option in the
  # luks.device."yourdevice".If the keyfile exists it will be used to
  # decrypt the disk, if not it will ask for the passphrase.
  # We immediately delete the LUKS slot after mounting.

  systemd.services."prepare-kexec" = {
    path = with pkgs; [ cpio cryptsetup gzip ];
    script = lib.mkForce ''
      set -euo pipefail
      umask 0077

      # Don't load the current system profile if we already have a 
      # kernel loaded.
      if [[ 1 = "$(</sys/kernel/kexec_loaded)" ]] ; then
          echo "kexec kernel has already been loaded, prepare-kexec skipped"
          exit 0
      fi

      p=$(readlink -f /nix/var/nix/profiles/system)
      if ! [[ -d $p ]]; then
        echo "Could not find system profile for prepare-kexec"
        exit 1
      fi

      if ! [[ -f "/LUKS-Passphrase-file.txt" ]]; then
        echo "Could not find luks-passphrase file"
        exit 1
      fi

      # add 256 random bytes temp key to the LUKS keyslot 31
      TEMP_DIR="$(mktemp -d --tmpdir=/dev/shm)"
      mkdir "$TEMP_DIR/etc"
      head -c 256 /dev/urandom>"$TEMP_DIR/etc/tmp-passphrase"
      cryptsetup luksAddKey --batch-mode --key-slot 31 ${luksDevice} "$TEMP_DIR/etc/tmp-passphrase"</LUKS-Passphrase-file.txt

      # create a new cpio archive and append it to the original initrd
      cd "$TEMP_DIR"
      cp "$p/initrd" "$TEMP_DIR/initrd.img"
      find etc | cpio -H newc -o | gzip >> "$TEMP_DIR/initrd.img"

      # load the kernel with the new initrd
      kexec --load "$p/kernel" --initrd="$TEMP_DIR/initrd.img" --append="$(cat "$p/kernel-params") init=$p/init"
    '';
  };

  boot = {
    initrd = {
      luks.devices."yourdevice" = {
        fallbackToPassword = true;
        keyFile = "/etc/tmp-passphrase";
      };
      systemd.services.clear-luks-keyslot = {
        description = "Clear the LUKS key slot after successful kexec";
        wantedBy = [ "initrd.target" ];
        after = [ "systemd-cryptsetup@root.service" ];
        serviceConfig.Type = "oneshot";
        path = with pkgs; [ cryptsetup ];
        script = "cryptsetup luksKillSlot --batch-mode ${luksDevice} 31 || true";
    };
  };
}