DEV Community

Vehbi Sinan Tunalioglu
Vehbi Sinan Tunalioglu

Posted on • Originally published at thenegation.com

NixOS on Raspberry Pi 4 with Encrypted Filesystem

This guide documents how to install NixOS on Raspberry Pi 4 with an
encrypted root filesystem.

Background and Motivation

First, a little bit of background:

  • Raspberry Pi 4 is a single-board computer based on ARM architecture with enough CPU power and RAM capacity that allows running a good deal of computational tasks.
  • NixOS is a GNU/Linux operating system distribution that is built on top of the Nix Package Manager enabling users to declaratively configure their systems and reproduce such configuration on different hosts or the same host over time using rollbacks.
  • The encrypted filesystem is a security practice that allows users to encrypt their entire root filesystem or parts of it to ensure that third parties can not extract information without the encryption key from the underlying media (physical or virtual) once taken offline.

I have access to a Raspberry Pi 4 with 8GB of RAM and an external
SSD. It proved to be quite usable as a workstation. I tested it to see
if I can:

  1. run productivity tools such as a Web browser or an office suite,
  2. perform work activities such as Haskell development using Emacs and lsp-haskell or React-based Web application development using VS Code, and
  3. deploy services using Docker and Docker Compose.

To my surprise, I found it quite usable. A significant number of tasks
can be performed without noticing that the host is a computer costing
less than 100 dollars. It should be noted that SSD is quite critical,
though. Because using a micro-SD card for the root filesystem will
ruin the entire experience.

On the other hand, I can reproduce my NixOS-based workstation
configuration easily on different hosts. I already have almost
identical workstations in my reach. Almost, because certain states
can not be easily reproduced or the benefits are not worth the hassle
(such as ad-hoc data files, Web browser cache or ephemeral
development, build and testing artefacts).

So, I decided to setup a new workstation on my Raspberry
Pi 4. However, it would not be an option for me without an encrypted
root filesystem. I thought that it would be possible but tedious. It
turned out to be quite straightforward.

The rest of this guide documents how to install NixOS on Raspberry Pi
4 (rpi4) with an encrypted root filesystem in a step-by-step
fashion.

Installation Media

Download SD card image from Hydra:

https://hydra.nixos.org/job/nixos/release-23.05/nixos.sd_image.aarch64-linux

This guide specifically uses this file from 2023-10-26:

nixos-sd-image-23.05.4527.60b9db998f71-aarch64-linux.img.zst
Enter fullscreen mode Exit fullscreen mode

... from:

https://hydra.nixos.org/build/239386976

... by issuing the command:

nix-shell -p aria2 --run "aria2c -c --dir /tmp/nixos-image/ https://hydra.nixos.org/build/239386976/download/1/nixos-sd-image-23.05.4527.60b9db998f71-aarch64-linux.img.zst"
Enter fullscreen mode Exit fullscreen mode

Then, uncompress the image:

nix-shell -p zstd --run "unzstd /tmp/nixos-image/nixos-sd-image-23.05.4527.60b9db998f71-aarch64-linux.img.zst"
Enter fullscreen mode Exit fullscreen mode

Find the USB disk (or micro-SD card) device path:

lsblk
Enter fullscreen mode Exit fullscreen mode

Write the image:

sudo dd if=/tmp/nixos-image/nixos-sd-image-23.05.4527.60b9db998f71-aarch64-linux.img of=/dev/<YOUR_DISK_OR_CARD>
Enter fullscreen mode Exit fullscreen mode

Booting

You need to:

  1. boot from the image you burned above,
  2. attach the actual disk you want to install to (you can do this later, too),
  3. attach a keyboard and a monitor, and
  4. have access to a wired or wireless network.

As for network access, this guide assumes a wired network so as not to
conflate the post any further. I used a wireless network myself first,
and then, reproduced instructions later on a wired network.

Once you successfully boot your rpi4 from your NixOS image, follow the
subsections below.

Network Configuration

Check if the network connection is established:

ping google.com
Enter fullscreen mode Exit fullscreen mode

Find the interface name and the IP address assigned to it:

ip a
Enter fullscreen mode Exit fullscreen mode

In my case, and most likely in your case, it is eth0:

ip a show dev eth0
Enter fullscreen mode Exit fullscreen mode

Note the IP address, because we will use it in the next step.

SSH Connection

We want to continue the installation from a proper
workstation. Luckily, the installation media already launched an SSH
service. Therefore, we will setup authorised SSH keys, switch to the
workstation and continue installation there.

First, add your SSH keys to your rpi4. I am using my SSH public keys
stored on my GitHub account:

mkdir ~/.ssh
chmod 700 ~/.ssh
curl https://github.com/<GITHUB-USERNAME>.keys -o ~/.ssh/authorized_keys
Enter fullscreen mode Exit fullscreen mode

Now, switch to your workstation, and connect to your rpi4:

ssh nixos@<RPI4-IP-ADDRESS>
Enter fullscreen mode Exit fullscreen mode

Optionally, launch a screen on your rpi4 and follow the installation
procedure inside:

screen -S INSTALLATION
Enter fullscreen mode Exit fullscreen mode

Partitioning

First, connect the disk that you will install the root filesystem
to. Then, check device information:

lsblk
Enter fullscreen mode Exit fullscreen mode

In my case, it is /dev/sda. Find yours, and export it as a shell
environment variable:

export DEVICE_ROOT="/dev/sda"
Enter fullscreen mode Exit fullscreen mode

Also, export your installation media disk path (in my case, it is
/dev/sdb):

export DEVICE_INST="/dev/sdb"
Enter fullscreen mode Exit fullscreen mode

Next, wipe the data on the disk:

sudo wipefs -a "${DEVICE_ROOT}"
Enter fullscreen mode Exit fullscreen mode

And create 2 partitions (one for boot partition and one for LUKS
partition):

sudo parted "${DEVICE_ROOT}" -- mklabel gpt
sudo parted "${DEVICE_ROOT}" -- mkpart ESP fat32 1MiB 512MiB
sudo parted "${DEVICE_ROOT}" -- set 1 boot on
sudo parted "${DEVICE_ROOT}" -- mkpart primary 512MiB 100%
Enter fullscreen mode Exit fullscreen mode

Note that parted may warn you about optimal disk alignment. You
may ignore them. This is possibly due to the quality of the USB disk
enclosure or connector. It seems that some USB-connected disks are
misrepresented in their optimal configuration, which confuses the
alignment calculations and results in bogus warnings. I do not know
any further.

Now, setup the encrypted LUKS partition, and open it:

sudo cryptsetup luksFormat "${DEVICE_ROOT}2"
sudo cryptsetup luksOpen "${DEVICE_ROOT}2" enc-pv
Enter fullscreen mode Exit fullscreen mode

Next, we will create two logical volumes on the LUKS partition:

  1. One for a 8GB swap partition, and
  2. The rest for our root filesystem.
sudo pvcreate /dev/mapper/enc-pv
sudo vgcreate vg /dev/mapper/enc-pv
sudo lvcreate -L 8G -n swap vg
sudo lvcreate -l '100%FREE' -n root vg
Enter fullscreen mode Exit fullscreen mode

Format the partitions and volumes:

sudo mkfs.fat "${DEVICE_ROOT}1"
sudo mkfs.ext4 -L root /dev/vg/root
sudo mkswap -L swap /dev/vg/swap
Enter fullscreen mode Exit fullscreen mode

Then, mount filesystems:

sudo mount /dev/vg/root /mnt
sudo mkdir /mnt/boot
sudo mount "${DEVICE_ROOT}1" /mnt/boot
sudo swapon /dev/vg/swap
Enter fullscreen mode Exit fullscreen mode

Finally, copy the firmware from your installation media to your boot
partition:

sudo mkdir /firmware
sudo mount "${DEVICE_INST}1" /firmware
sudo cp /firmware/* /mnt/boot
sudo umount /firmware
Enter fullscreen mode Exit fullscreen mode

Installation

Generate NixOS configuration:

sudo nixos-generate-config --root /mnt
Enter fullscreen mode Exit fullscreen mode

Now, note the device ID of unencrypted ${DEVICE_ROOT}2 (/dev/sda2
in my case, 37e51822-c30e-4027-a746-562778c20df0):

lsblk --fs
Enter fullscreen mode Exit fullscreen mode

Now, change the below according to your needs (let section should be
enough) and put it in your /mnt/etc/nixos/configuration.nix file on
rpi4:

{ pkgs, ... }:

let
  luksroot = "/dev/disk/by-uuid/37e51822-c30e-4027-a746-562778c20df0";
  hostname = "pixos";
  timezone = "UTC";
  username = "patron";
  sshkeys = [
    "ssh-ed25519 AAAAC3Nz..."
    "ssh-ed25519 AAAAC3Nz..."
  ];
in
{
  ## Perform required imports:
  imports = [
    ./hardware-configuration.nix
  ];

  ## Boot configuration:
  boot = {
    ## Boot loader configuration:
    loader = {
      ## Disable GRUB to use the extlinux boot loader (NixOS wants to enable GRUB by default):
      grub.enable = false;

      ## Enables the generation of /boot/extlinux/extlinux.conf:
      generic-extlinux-compatible.enable = true;
    };

    ## Use the kernel packaged for Raspberry Pi 4:
    kernelPackages = pkgs.linuxKernel.packages.linux_rpi4;

    ## Configure initrd for LUKS root:
    initrd.luks.devices.luksroot = {
      device = luksroot;
      preLVM = true;
      allowDiscards = true;
    };
  };

  ## Set the timezone:
  time.timeZone = timezone;

  ## Set default locale:
  i18n.defaultLocale = "en_US.UTF-8";

  ## Configure console:
  console = {
    font = "Lat2-Terminus16";
    keyMap = "us";
  };

  ## Configure networking:
  networking = {
    ## Set the hostname:
    hostName = hostname;

    ## Enable Network Manager:
    networkmanager.enable = true;

    ## Enable and configure firewall:
    firewall = {
      enable = true;
      allowedTCPPorts = [ ];
      allowedUDPPorts = [ ];
    };
  };

  ## Setup services:
  services = {
    ## Enable the OpenSSH service:
    openssh.enable = true;
  };

  ## Install system-wide packages:
  environment.systemPackages = [
    pkgs.vim
    pkgs.wget
  ];

  ## Create a user:
  users.users."${username}" = {
    isNormalUser = true;
    extraGroups = [
      "wheel"
    ];
    packages = [ ];
    openssh.authorizedKeys.keys = sshkeys;
  };

  # This value determines the NixOS release from which the default
  # settings for stateful data, like file locations and database versions
  # on your system were taken. It's perfectly fine and recommended to leave
  # this value at the release version of the first install of this system.
  # Before changing this value read the documentation for this option
  # (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
  system.stateVersion = "23.05"; # Did you read the comment?
}
Enter fullscreen mode Exit fullscreen mode

Then, install:

time sudo nixos-install --no-root-password
Enter fullscreen mode Exit fullscreen mode

Set a root password:

sudo passwd --root /mnt root
Enter fullscreen mode Exit fullscreen mode

Reboot:

sudo shutdown -r now
Enter fullscreen mode Exit fullscreen mode

If things go wrong...

If the system does not boot, and you need to reconfigure it using the
installation media, reboot from the installation media.

Then, note the disk ID as it may have changed:

lsblk --fs
Enter fullscreen mode Exit fullscreen mode

In my case, it is still /dev/sda. Find yours, and export it as a
shell environment variable:

export DEVICE_ROOT="/dev/sda"
Enter fullscreen mode Exit fullscreen mode

Open encrypted luks:

sudo cryptsetup luksOpen "/dev/${DEVICE_ROOT}2" enc-pv
Enter fullscreen mode Exit fullscreen mode

Mount stuff:

sudo lvchange -a y /dev/vg/swap
sudo lvchange -a y /dev/vg/root
sudo mount /dev/vg/root /mnt
sudo mount "${DEVICE_ROOT}1" /mnt/boot
sudo swapon /dev/vg/swap
Enter fullscreen mode Exit fullscreen mode

Make your changes and re-install:

time sudo nixos-install
Enter fullscreen mode Exit fullscreen mode

That's it.

Top comments (0)