Imperative & Declarative Wifi networks with wpa_supplicant

I recently opened wpa_supplicant: allow both imperative and declarative networks by Ma27 · Pull Request #113716 · NixOS/nixpkgs · GitHub which aims to allow both declarative networks (i.e. defining wifi networks for wpa_supplicant in networking.wireless.networks) and imperative networks (i.e. writing them imperatively to /etc/wpa_supplicant.conf, e.g. via wpa_gui).

The reason for this is that I have a fair share of networks that I want to have “just available” on my config and more sensitive things (such as WPA2 enterprise credentials for university and my employer’s office) that I don’t want to have in the store on my laptop. Right now, you have to choose between imperative or declarative networks, but can’t use both which is something this PR aims to change.

First of all, let me explain how:

  • wpa_supplicant has two config files (the “default one” specified with -c) and a second one specified with -I.
    • The second one is only supposed to be used for “global” settings, however it’s possible to write networks into it.
    • However all networks (from both files) will be written to /etc/wpa_supplicant.conf e.g. when wpa_gui instructs the daemon to save all changes. This also means, that declarative networks would basically “become” imperative networks if you use -I for a store-path and -c for imperative networks.
  • I worked around this by patching wpa_supplicant to ignore networks from the file specified with -I, so wpa_gui and wpa_supplicant treat these as “immutable” while you can still declare networks (and also custom settings) imperatively in /etc/wpa_supplicant.conf.

I’m writing this up here because I’d love to get a bit more feedback on this. I know that patching wpa_supplicant here for basically a new feature is fairly invasive, so I don’t want to see this in master before getting more feedback. I already had a longer discussion with @bb2020 about this which may provide more context. We also agreed that before this is fully ready, the behavior should be made opt-in at least.

I’m already using this patch for a while without any issues so far. But would be cool to get a few more opinions on this :slight_smile:

1 Like

Some random thoughts:

  • NetworkManager is a frontend to networking tools, and IIRC it’s in the process of switching from wpa_supplicant backend to iwd. the iwd backend is already supported in NixOS.
  • What are the opportunities for connecting to wifi networks securely using the secrets managers?
1 Like

First of all, thanks a lot for your input!

NetworkManager is a frontend to networking tools, and IIRC it’s in the process of switching from wpa_supplicant backend to iwd 2. the iwd backend is already supported in NixOS.

Well, I had enough of iwd and switched back to wpa_supplicant a while ago, so nothing I’m particularly interested in :slight_smile:

Also this change only applies to wpa_supplicant (the module and the package) itself. Not sure if e.g. declarative are even possible with network manager or am I misunderstanding your point here?

What are the opportunities for connecting to wifi networks securely using the secrets managers 3?

I think it would be possible to use e.g. wpa_supplicant -I /run/secrets/mynetworks.conf in cojunction with my patch (to avoid that these are written to /etc/wpa_supplicant.conf for the reasons I outlined in my OP).

Please keep in mind that this wasn’t supported before and is IMHO out of scope for the current PR. But don’t get me wrong, I’d be open to discuss this as well in the future!

1 Like

It should be possible to put a file in /etc/NetworkManager/system-connections/mywifi.nmconnection declaratively.

I marked the PR as “ready for review” now. The behavior is still opt-in of course. I’ll wait for a while, but I’d consider this as ready now (I’m also running it on my machine for >2 months).

I went another way, which is to autogenerate the /etc/NetworkManager/system-connections/*.nmconnection files, by using the following module :

{ config, lib, pkgs, ... }:

with lib;

let
  cfg = config.networking.networkmanager;

  getFileName = stringAsChars (x: if x == " " then "-" else x);

  createWifi = ssid: opt: {
    name = ''
      NetworkManager/system-connections/${getFileName ssid}.nmconnection
    '';
    value = {
      mode = "0400";
      source = pkgs.writeText "${ssid}.nmconnection" ''
        [connection]
        id=${ssid}
        type=wifi

        [wifi]
        ssid=${ssid}

        [wifi-security]
        ${optionalString (opt.psk != null) ''
        key-mgmt=wpa-psk
        psk=${opt.psk}''}
      '';
    };
  };

  keyFiles = mapAttrs' createWifi config.networking.wireless.networks;
in {
  config = mkIf cfg.enable {
    environment.etc = keyFiles;

    systemd.services.NetworkManager-predefined-connections = {
      restartTriggers = mapAttrsToList (name: value: value.source) keyFiles;
      serviceConfig = {
        Type = "oneshot";
        RemainAfterExit = true;
        ExecStart = "${pkgs.coreutils}/bin/true";
        ExecReload = "${pkgs.networkmanager}/bin/nmcli connection reload";
      };
      reloadIfChanged = true;
      wantedBy = [ "multi-user.target" ];
    };
  };
}

with

networking.networkmanager.enable = true;
networking.wireless.networks."SSID" = { psk = "KEY" };
3 Likes

@Ma27 do you have any input on why your approach would be preferred (or not) when compared to @hpoussin’s offered solution?

Hm, actually, upon further consideration, maybe someone has an approach that composes with something like sops-nix or age-nix to keep the PSK out of the store?

I guess either approach could maybe be altered to build the config file on the fly at “run-time” from something from /run/secrets

EDIT: I guess if one just sticks the entire nmconnection file into the store encrypted, then configures sops-nix to symlink it where NM expects then it might just work.

1 Like

@hpoussin: Thanks a lot for this module that worked very well for me.

I had to join lines:

 name =''Network ...nmconnection''

in a single line to make it work unless the extension of the generated file was followed by garbage chars;

Also, I think there is a little typo there:

networking.wireless.networks."SSID" = { psk = "KEY" };

replaced by

networking.wireless.networks."SSID" = { psk = "KEY"; };  # (missing semi-colon after "KEY")

I have finally my network credentials declaratively with this !