An impermanent NixOS Setup is something, I personally I really enjoy, as I don’t like preserving everything on my system forever … Persistence Opt-In is the best approach.

And recently I came across a nice Blog about hardening NixOS from Xe Iaso’s blog. And well one thing especially seemed cool, so I wanted to share it.

NoExec for the Root FS

At the bottom of the blog is a part about having noexec for the root fs (which is a tmpfs anyway). Since all programs in NixOS life anyway in /nix/store and /nix is its own partition in the typical Impermancence Setup (I won’t talk about how to this now, as there are already many good blogs discussing this, and showing how it’s done; if you want to see my own setup, check my Codeberg out). So only the /nix parititon needs to be mounted without noexec, the rest can be noexec.

Additionally you can even add nodev and nosuid to the root fs, since devices should life under /dev, which is its own partition anyway (a temporary device fs). And suid (switch user id) requires the partition to allow executables anyway.

disko.devices.nodev = {
  "/" = {
    fsType = "tmpfs";
    mountOptions = [
      "size=50%"
      "mode=755"

      "noexec"
      "nodev"
      "nosuid"
    ];
  };
};

I use this setup for a while now and so far I haven’t run into any issues with it. So I can only recommend it to anyone, since its certainly not wrong, to minimise the attack surface (even though this is obviously not a here you go, don’t worry anymore about anything else). It just stops things like running a C Binary that is placed into e.g. /tmp via /tmp/malicious_binary but not sth like bash /tmp/malicious_script.sh

Boot Partition

These options can also be added to the /boot Parititon without issues.

content.partitions.esp = {
  name = "ESP";
  size = "1G";
  type = "EF00";

  content = {
    type = "filesystem";
    format = "vfat";
    mountpoint = "/boot";
    mountOptions = [
      "fmask=0177"
      "dmask=0077"
      "noexec"
      "nosuid"
      "nodev"
    ];
  };
};

Persistent Partition

Well if you don’t have any binaries you want to persist, you can also add these options there too, if you have some like through steam games, flatpaks etc. you can at least add the nodev and nosuid most of the times. Also if you’re writing code yourself that is compiled, you probably don’t want noexec for these files.

My config looks sth like: (Note: these are already btrfs subvolumes, since I encrypt my whole btrfs fs via luks, so using subvolumes is kinda convenient -> only one decrypt)

subvolumes = {
  "/persistent" = {
    mountOptions = [
      "subvol=persistent"
      "noatime"

      "nosuid"
      "nodev"
    ];
    mountpoint = "/persistent";
  };
  "/nix" = {
    mountOptions = [
      "subvol=nix"
      "noatime"
    ];
    mountpoint = "/nix";
  };
};