Initrd disk decryption via tailscale on iwd

Hey all! Guides might not be the best place for this, since I’m not sure anyone else will ever do it… but hopefully it makes googling the things I tried to google easier for a future person. And also hopefully ElvishJerrico, whose name I ran into on nearly every thread I clicked during this and gained a ton of respect for, will tear it apart in 30 seconds and make it much better :wink:

Preface

At this point, I’m not sure why I didn’t go with wpa_supplicant, but it probably would’ve been much easier. Some drivel like “Stage 1/2 parity, shiny and new”… in any case, I didn’t go down this route and thus have no advice, but if you’re reading this I’d still recommend you do.

Additionally, a caveat. I store three files in a small, unencrypted partition to make this work:

  • tailscale.key, which holds a reusable, ephemeral, tagged tailscale auth key. A very key point is that in my tailscale settings, I don’t allow any outgoing connections from machines added using this key. This makes a stolen key fairly useless.
  • wifi, which holds the name of the iwd conf file
  • an iwd conf file

This double-file setup keeps the name of my wifi network out of my public nix repo, and secondarily means changing wifi networks doesn’t require a rebuild.

Also, all code blocks will be namespaced under boot.initrd.

Mounting the keys partition

There exists the option initrd.supportedFilesystems, but I couldn’t tell if it worked with systemd stage 1. Thus, I added the ext4 kernel modules myself.

availableKernelModules = [ "ext4" "jbd2" "mbcache" "crc16" ];
systemd.mounts = [{
  what = "/dev/disk/by-partlabel/disk-main-keys";
  where = "/keys";
  type = "ext4";
}];

This makes keys.mount available in the initrd, which contains the files listed above.

Get iwd working

We need an actual binary to run! There’s three options that ostensibly do this, and I never did properly figure out the difference. For iwd, I used all of them:

systemd.packages = [ pkgs.iwd ];
systemd.initrdBin = [ pkgs.iwd ];
systemd.storePaths = [ "${pkgs.iwd}" ] # <- list of string!

As I understand it, packages will pull in the provided iwd.service file and make it available.

Awesome! So, lets reboot and run it, and… hmm

Get iwd working, part 2: kmod boogaloo

iwd complains about missing CRYPTO_CONFIG stuff. This turned out to be some more kernel mods to load. I found them and their dependencies through modinfo, leading to this:

availableKernelModules = [
  "af_alg" "algif_hash" "algif_skcipher"
  "md5" "sha1" "sha256" "sha512"
  "ecb" "cbc" "cmac" "hmac" "des" "libdes"
];

Awesome! So, lets reboot and run it, and… hmm

Get iwd working, part 3: dbus beegalee

Now iwd complains about Name request failed and exits. I know just enough to suspect this is a dbus thing, but not enough to do anything about it. Time to go down the rabbit hole.

Let’s just include the provided dbus file!

First, I had to find where to put it. This turned out to be /etc/dbus-1/system.d, I… think. If this weren’t a NixOS initrd, at least. In any case, with this info, I can just put it there, right?

How naive, past me. There are apparently so many ways to accomplish this:

  • initrd.extraFiles
  • initrd.prepend
  • initrd.systemd.contents
  • initrd.systemd.tmpfiles
  • initrd.secrets
  • handwritten service file that links a dbus file included via systemd.storePaths into system.d (yes, I tried this)

In most cases, nothing even showed up in /etc/dbus-1. But on one of them (thankfully!) I did get the file to show up! And… nothing. No change in the error. Hmm

Let’s tell nix to include the provided dbus file!

After a bit of reading the files in dbus-1 and googling, I realized the problem: the system.conf file wasn’t searching anywhere for extra files, and it didn’t happen automatically like I thought. So I need to get an <includedir> directive in there somehow.

After much searching, I found services.dbus.packages which promised to do exactly that! Give it a list of packages and it appends some <includedir>s in system.conf for them. And since other parts of the initrd inherit config from stage 2, maybe this will just work!

Mmm, no.

Ok what even

So. I realized that I needed to find where /etc/dbus-1 was even coming from, and my goodness, did it take some work. After a ton of loading my flake into nix repl, searching the /nix/store, and reading the nixpkgs source, I found it. initrd’s /etc/dbus-1 is generated in dbus.nix using a systemd.contents."/etc/dbus-1" call set to the result of a function, specifically makeDBusConf.

Well that’s… scary. I have no idea how important this function is, nor what it does. But hey, what have I got to lose? Lets replicate it to the best of our ability, include iwd, and see what happens.

After a bit of reading the makeDBusConf source, I settled on including iwd in the serviceDirectories list, and stuck this in my config:

systemd.contents."/etc/dbus-1".source = lib.mkForce (pkgs.makeDBusConf.override {
  dbus = pkgs.dbus;
  suidHelper = "/bin/false"
  serviceDirectories = [
    pkgs.dbus
    config.boot.initrd.systemd.package
    pkgs.iwd # <- there it is!
  ];
});

Now just reboot, try it out, and… oh. OH, my god, it worked. I would’ve accepted a new error, but it just straight up worked. The elation I felt at this point was incredible.

A brief aside about systemd-resolved, which probably won’t affect most people:

Summary

I’m not sure on the particulars because networking isn’t my strong suit, but even though my wifi connected, I wasn’t able to ping google. I think this is because of setting DNSOverTLS = true in my resolved settings, but it also might be because I use 9.9.9.9 as my DNS. In any case, I had to set this:

systemd.contents."/etc/systemd/resolved.conf".text = lib.mkForce ''
  [Resolve]
  DNS=1.1.1.1
  FallbackDNS=8.8.8.8
'';

Get tailscale working

This was much simpler. I ripped the configuration nearly wholesale from this github repo, only adding an --auth-key=file:/keys/tailscale.key argument in tailscaled-autoconnect. See the full configuration at the bottom, noting the tailscale storePaths

The final hurdle

At this point, I rebooted and et voila: my initrd showed up in the tailscale machines page with all the settings I expected it to have. I ssh’d in from my laptop, and… pam failure with an immediate disconnect.

Well, I know absolutely nothing about pam. But I don’t need to be super concerned about security here, since only people authenticated to my tailnet can even access the initrd… Maybe I can just wing this?

In the end, this took a single try. Here are the additions:

systemd.contents."/etc/pam.d/other".text = ''
  auth required pam_permit.so
  account required pam_permit.so
  password required pam_permit.so
  session required pam_permit.so
'';

systemd.storePaths = [
  "${pkgs.pam}/lib/security/pam_permit.so"
  "${pkgs.pam}/lib/libpam.so.0"
];

And it worked! Just like that. I know just enough about pam now to know I shouldn’t try and explain it. I would recommend just googling linux pam like I did, lol.

Final thoughts

Once again, I have no idea why I took the hard road here. It does feel pretty awesome to have gotten it working, though.

Systemd stage 1 has been pretty cool to work with, though I actually have no experience messing around with an initrd prior to this project, so take it with a grain of salt. I’m also not sure how well I ordered my various services, and it could probably be improved.

Overall, I’m just surprised that nothing feels too hacky, which is something I despise in my config. Even though I’m doing some pretty hacky things. Weird.

The final config, debug comments and all:

Summary
boot.kernelParams = [ "rd.systemd.debug_shell" ];
boot.initrd = {
  availableKernelModules = [
    "ccm" "ctr" "xhci_pci" "ahci" "nvme" "usbhid" "usb_storage" "sd_mod"
    "iwlwifi" "e1000e" "iwlmvm" # wifi
    "tun" # tailscale
    "ext4" "jbd2" "mbcache" "crc16" # ext4
    "af_alg" "algif_hash" "algif_skcipher" "ecb" # iwd
    "md5" "cbc" "sha256" "cmac" "hmac" "sha1" "sha512" # iwd
    "des" "libdes" # des for iwd
  ];

  systemd = 
    let
      interface = "wlan0";
    in {
      users.root.shell = "/bin/systemd-tty-ask-password-agent";

      contents = {
        # insane
        "/etc/dbus-1".source = lib.mkForce ( pkgs.makeDBusConf.override {
          # taken wholesale from dbus.nix (see boot.initrd.systemd section), as of
          # nixpkgs#d2c949d725459d0dc2160f910b28cd2625c1d552

          # see also makeDBusConf/package.nix, hereafter mkDB

          # inherit (cfg) apparmor;
            # in mkDB, this is "disabled" if left null.
            # WARN for posterity in case I ever use apparmor, for now leaving null
            # (to not deal with finding (cfg) access)

          dbus = pkgs.dbus; # hopefully this works

          suidHelper = "/bin/false"; # unchanged

          serviceDirectories = [
            pkgs.dbus # translated from cfg.dbusPackage (hopefully)
            config.boot.initrd.systemd.package # unchanged, should translate
            pkgs.iwd
          ];


        } );

        "/etc/systemd/resolved.conf".text = lib.mkForce ''
          [Resolve]
          DNS=1.1.1.1
          FallbackDNS=8.8.8.8
        '';

        "/etc/pam.d/other".text = ''
          auth required pam_permit.so
          account required pam_permit.so
          password required pam_permit.so
          session required pam_permit.so
        '';
      };

      

      mounts = [{
        what = "/dev/disk/by-partlabel/disk-main-keys";
        where = "/keys";
        type = "ext4";
      }];

      targets = {
        initrd = {
          wants = [ "network.target" ];
        };
      };

      packages = [ pkgs.iwd ];
      initrdBin = [ pkgs.iwd pkgs.unixtools.ping pkgs.util-linux ];

      network = {
        enable = true;
        wait-online.anyInterface = true;
        networks = config.systemd.network.networks; # TODO this can be better encapsulated/
        # insulated (to allow more seamless den aspects). basically it currently expects aspects.networking
        # to exist and provide this info
      };

      storePaths = [
        "${pkgs.tailscale}/bin/tailscaled"
        "${pkgs.tailscale}/bin/tailscale"
        "${pkgs.iproute2}/bin/ip"
        "${pkgs.iwd}"
        "${pkgs.pam}/lib/security/pam_permit.so"
        "${pkgs.pam}/lib/libpam.so.0"
      ];

      services = {
        iwd-files = {
          requiredBy = [ "iwd.service" ];
          before = [ "iwd.service" ];
          after = [ "keys.mount" ];
          requires = [ "keys.mount" ];
          unitConfig.DefaultDependencies = false;
          script = ''
            /bin/mkdir -p /var/lib/iwd

            /bin/ln -sf -t /var/lib/iwd /keys/"$(cat /keys/wifi)"
          '';
        };

        # these are overrides to the provided iwd.service from pkgs.iwd
        iwd = {
          requiredBy = [ "network.target" ];
          unitConfig.DefaultDependencies = false;
        };

        tailscaled = {
          wantedBy = [ "initrd.target" ];
          path = [ pkgs.iproute2 ];
          after = [ "network.target" ];
          unitConfig.DefaultDependencies = false;
          before = [ "shutdown.target" ];
          conflicts = [ "shutdown.target" ];

          serviceConfig = {
            Type = "notify";
            RuntimeDirectory = "tailscale";
            ExecStart = "${pkgs.tailscale}/bin/tailscaled --state=mem: --statedir=/run/tailscale --socket=/run/tailscale/tailscaled.sock --tun=userspace-networking";
            Restart = "on-failure";
          };
        };

        tailscaled-autoconnect = {
          wantedBy = [ "initrd.target" ];
          after = [
            "tailscaled.service"
            "network-online.target"
          ];
          wants = [ "network-online.target" ];
          requires = [ "tailscaled.service" ];

          unitConfig.DefaultDependencies = false;
          before = [ "shutdown.target" ];
          conflicts = [ "shutdown.target" ];

          serviceConfig = {
            Type = "oneshot";
            RemainAfterExit = true;
            Restart = "on-failure";
            RestartSec = 5;
          };
          script = ''
            ${pkgs.tailscale}/bin/tailscale --socket=/run/tailscale/tailscaled.sock up \
            --hostname=${host.hostName}-initrd \
            --auth-key=file:/keys/tailscale.key \
            --netfilter-mode=off \
            --ssh
          '';
        };
      };
    };
};

1 Like