Wireless connection within initrd

Hello everyone,

My NixOS home server has encrypted disks that I want to decrypt at boot time and remotely. For that purpose, I plan to use an initrd with ssh (options boot.initrd.network.ssh.enable is just fine for that), but with the extra constraint that my device internet connection is wireless (and I can’t really replace this with a wired connection for now).

I haven’t seen any wpa_supplicant/networkmanager option that would integrate nicely with the settings provided for boot.initrd, nor did I find anything particularly useful nix-wise on the internet, so I was wondering if any of you would be so kind to share their experience with that (and possible config files :slight_smile: ), if any.

Thanks!

1 Like

OK so I’ve been trying to set up something with networkd, but it still doesn’t work. I can see in journalctl that the SSH server goes up after network.target is reached, but networkd never brings the WLAN interface up:

localhost systemd-udevd[130]: Using default interface naming scheme 'v253'.
localhost systemd[1]: Started Journal Service.
localhost systemd-networkd[139]: lo: Link UP
localhost systemd-networkd[139]: lo: Gained carrier
localhost systemd-networkd[139]: Enumeration completed
localhost systemd[1]: Starting Create Volatile Files and Directories...
localhost systemd[1]: Started Network Configuration.
localhost systemd[1]: Reached target Network.
localhost systemd[1]: Starting SSH Daemon...
localhost systemd[1]: Started SSH Daemon.

So the SSH server cannot be reached. My configuration is the following:

  boot.initrd = {
    systemd = {
      enable = true;
      packages = [ pkgs.wpa_supplicant ];
      contents."/etc/wpa_supplicant/wpa_supplicant-wlp2s0.conf".source =
        /root/wpa_supplicant.conf;

      network.enable = true;
      network.networks."10-wlan" = {
        matchConfig.Name = "wlp2s0";
        networkConfig.DHCP = "yes";
      };
    };

    network = {
      enable = true;
      ssh = {
        enable = true;
        port = 22;
        hostKeys = [ "/etc/ssh/ssh_host_ed25519_key" ];
        authorizedKeys = default.user.openssh.authorizedKeys.keys;
      };
    };
  };

I’ve checked the ram disk directly and the content is consistent with my configuration. I’ve also tried other interface names like wlan0 in case it’s not the predictive names that are used during stage 1 but no dice. so I guess there’s something I’m doing wrong with the interaction between networkd and wpa_supplicant.

systemd initrd doesn’t automatically pull in binaries for you, so you probably need to add boot.initrd.systemd.initrdBin = [ pkgs.wpa_supplicant ]; or something.

Thanks for your answer. I did add both

services.systemd-networkd.after = [ "wpa_supplicant@wlan0.service" ]; # switched to wlan0 as it seems to be the correct interface name during stage 1

and

initrdBin = [ pkgs.wpa_supplicant ];

(which added the binaries that were indeed missing from the ram disk) in boot.initrd.systemd but still, nothing… The wpa_supplicant@wlan0 service doesn’t even have an entry in the journal.

You probably need wants instead of or in addition to after

Also, keep in mind that this will copy that conf file to the nix store (which makes it readable to all users), and into the initrd that lives unencrypted on /boot. So if this contains sensitive information like your wifi password, this could be a problem. Look into boot.initrd.secrets options instead.

Again, thanks for your answers. I tried all combinations of after, wants and requires for the dependency between systemd-networkd and wpa_supplicant@wlan0 but it didn’t work. More surprisingly, the SSH server won’t even start if I add a wants or requires. So I also tried adding

sshd.wantedBy = [ "cryptsetup.target" ]; # and/or before, requiredBy, ...

But then I get a cyclic dependency:

systemd[1]: network.target: Found ordering cycle on wpa_supplicant@wlan0.service/start
systemd[1]: network.target: Found dependency on sysinit.target/start
systemd[1]: network.target: Found dependency on cryptsetup.target/start
systemd[1]: network.target: Found dependency on sshd.service/start
systemd[1]: network.target: Found dependency on network.target/start
systemd[1]: network.target: Job wpa_supplicant@wlan0.service/start deleted to break ordering cycle starting with network.target/start

So I’m again out of luck. At this point I definitely understand that it’s a pure systemd skill issue on my part, as it’s true that I’ve never had to tweak service dependencies like that.

Look at how its done in stage 2 in nixos/modules/services/networking/wpa_supplicant.nix. It looks like this shouldn’t be ordered before networkd at all. And looking at the unit that’s available in the package, most of what you need is done already by it:

$ cat /nix/store/n3bkjcjccy9d712515819155d2629p3b-wpa_supplicant-2.10/etc/systemd/system/wpa_supplicant@.service
[Unit]
Description=WPA supplicant daemon (interface-specific version)
Requires=sys-subsystem-net-devices-%i.device
After=sys-subsystem-net-devices-%i.device
Before=network.target
Wants=network.target

# NetworkManager users will probably want the dbus version instead.

[Service]
Type=simple
ExecStart=/nix/store/n3bkjcjccy9d712515819155d2629p3b-wpa_supplicant-2.10/sbin/wpa_supplicant -c/etc/wpa_supplicant/wpa_supplicant-%I.conf -i%I

[Install]
WantedBy=multi-user.target

So I think all you need is:

boot.initrd.systemd.packages = [pkgs.wpa_supplicant];
boot.initrd.systemd.initrdBin = [pkgs.wpa_supplicant];
boot.initrd.systemd.targets.initrd.wants = ["wpa_supplicant@wlan0.service"];

And you can get rid of any ordering against systemd-networkd.service. I think networkd will just figure it out as it comes up?

You’re right, it seems to be better. I don’t understand why this is the case, I guess my vision of how systemd schedules tasks, with basic transitive dependencies, is a bit too naive. Anyway, I can see a startup job for /sys/subsystem/net/devices/wlan0 (which is a dependency for wpa_supplicant), but then it times out. I thought of a missing driver/kernel module but I see nothing network-related in my stage 2 kernel modules… Nonetheless I tried

boot.initrd.kernelModules = [ "iwlwifi" ]; # also tried availableKernelModules

(which corresponds to my driver, but again I don’t have to do this for stage 2) but then:

  • it changes nothing (the same timeout occurs);
  • surprisingly, it messes up the stage 2 connectivity and I can’t connect to my server at all.

Yea, you usually don’t see kernel modules for the stage 2 config. Most modules get loaded automatically when devices that need them are detected. This is the difference between boot.initrd.kernelModules and boot.initrd.availableKernelModules; the former are modules that need to be loaded explicitly, and the latter are modules that are included in initrd so they can be auto-loaded. Stage 2 doesn’t need an equivalent to availableKernelModules because all modules are available by way of being packaged with the kernel.

Anyway

The wpa_supplicant@.service file shipped by wpa_supplicant simply orders itself before network.target and after the device it’s trying to configure. That’s the only ordering controlling when it starts. It could very well start at the same time as systemd-networkd or even after; networkd will just figure out what to do when it sees the interface. The boot.initrd.systemd.targets.initrd.wants = ["wpa_supplicant@wlan0.service"]; line just says that this service needs to be started for the wlan0 interface during stage 1.

That’s all pretty strange… I’m not sure why that wouldn’t work. Probably need to start looking through logs.

Thanks for the explanations :slight_smile:

I had a look at the other network-related modules from intel, and so I added instead

availableKernelModules = [ "iwlwifi" "iwlmvm" ];

and now the interface seems to be correctly configured, and the connectivity remains in stage 2; I am confused as to why this is the case, given that iwlmvm is not listed as a dependency of anything in lspci -v, and I couldn’t even find on the internet what MVM means in this context.

But this still isn’t it, because the services load in an incorrect order: the SSH server starts correctly, but deliberately after the disk has been decrypted. Here are some selected lines from the log.

18:18:35 systemd[1]: Starting Cryptography Setup for luks-b38ebe53-0c81-4774-9e08-b32664e58084...
18:18:35 systemd-tty-ask-password-agent[217]: Starting password query on /dev/tty1.
18:18:36 systemd-networkd[138]: wlp2s0: Configuring with /etc/systemd/network/10-wlan.network.
18:18:36 systemd-networkd[138]: wlp2s0: Link UP
18:19:45 systemd-tty-ask-password-agent[217]: Password query on /dev/tty1 finished successfully.
18:19:48 systemd[1]: Finished Cryptography Setup for luks-b38ebe53-0c81-4774-9e08-b32664e58084.
18:19:48 systemd[1]: Reached target Local Encrypted Volumes.
18:19:48 systemd[1]: Starting NixOS Activation...
18:19:48 systemd[1]: Started WPA supplicant daemon (interface-specific version).
18:19:48 systemd[1]: Reached target Network.
18:19:48 systemd[1]: Starting SSH Daemon...
18:19:48 wpa_supplicant[289]: Successfully initialized wpa_supplicant
18:19:48 systemd[1]: Started SSH Daemon.
18:19:48 systemd[1]: initrd-nixos-activation.service: Deactivated successfully.
18:19:48 systemd[1]: Finished NixOS Activation.
18:19:48 systemd[1]: Reached target Initrd Default Target.
18:19:48 systemd[1]: Stopping SSH Daemon...

So again, I also tried adding additional systemd dependencies between services and targets, essentially adapting this page I’ve recently found, but I get about the same results.

Ah, interesting. So, I know what’s happening there. The wpa_supplicant@.service unit is waiting for sysinit.target because of the default dependencies of service units. This means waiting for encrypted devices. We had the same problem with sshd, and we solved it by just adding DefaultDependencies=false. You could do the same with the wpa_supplicant service with boot.initrd.systemd.services."wpa_supplicant@".unitConfig.DefaultDependencies = false; I think. But I do wonder if we should just change something so that encrypted devices aren’t a part of sysinit.target in initrd, since sysinit.target means something slightly different in initrd. Then neither you nor the sshd module would have to work around it

Oh, and that page seems basically entirely irrelevant to NixOS’s initrd configuration.

1 Like

Well, from a task-dependency perspective, it is still interesting. It also set DefaultDependencies=no btw, maybe for a similar reason? Anyway, it doesn’t matter, because now my config works :slight_smile:

I also had to adjust other settings, like including kernel modules for the cryptographic coprocessor. I inferred it from the errors I got from wpa_supplicant, but still it required some trials and errors; I couldn’t find any way of detecting this methodically (with some sort of explicit missing module report).

Here is the working config:

  boot.initrd = let interface = "wlp2s0"; in
    {
      # crypto coprocessor and wifi modules
      availableKernelModules = [ "ccm" "ctr" "iwlmvm" "iwlwifi" ];

      systemd = {
        enable = true;

        packages = [ pkgs.wpa_supplicant ];
        initrdBin = [ pkgs.wpa_supplicant ];
        targets.initrd.wants = [ "wpa_supplicant@${interface}.service" ];

        # prevent WPA supplicant from requiring `sysinit.target`.
        services."wpa_supplicant@".unitConfig.DefaultDependencies = false;

        users.root.shell = "/bin/systemd-tty-ask-password-agent";

        network.enable = true;
        network.networks."10-wlan" = {
          matchConfig.Name = interface;
          networkConfig.DHCP = "yes";
        };
      };

      secrets."/etc/wpa_supplicant/wpa_supplicant-${interface}.conf" =
        /root/secrets/wpa_supplicant.conf;

      network.enable = true;
      network.ssh = {
        enable = true;
        port = 22;
        hostKeys = [ "/etc/ssh/ssh_host_ed25519_key" ];
        authorizedKeys = default.user.openssh.authorizedKeys.keys;
      };
    };

Thanks a lot for your time and patience!

3 Likes