diff --git a/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml index ddb58eefe25c..b6e69da6d89c 100644 --- a/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml +++ b/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml @@ -1277,6 +1277,73 @@ Superuser created successfully. + + + The + networking.wireless + module (based on wpa_supplicant) has been heavily reworked, + solving a number of issues and adding useful features: + + + + + The automatic discovery of wireless interfaces at boot has + been made reliable again (issues + #101963, + #23196). + + + + + WPA3 and Fast BSS Transition (802.11r) are now enabled by + default for all networks. + + + + + Secrets like pre-shared keys and passwords can now be + handled safely, meaning without including them in a + world-readable file + (wpa_supplicant.conf under /nix/store). + This is achieved by storing the secrets in a secured + environmentFile + and referring to them though environment variables that + are expanded inside the configuration. + + + + + With multiple interfaces declared, independent + wpa_supplicant daemons are started, one for each interface + (the services are named + wpa_supplicant-wlan0, + wpa_supplicant-wlan1, etc.). + + + + + The generated wpa_supplicant.conf file + is now formatted for easier reading. + + + + + A new + scanOnLowSignal + option has been added to facilitate fast roaming between + access points (enabled by default). + + + + + A new + networks.<name>.authProtocols + option has been added to change the authentication + protocols used when connecting to a network. + + + + The diff --git a/nixos/doc/manual/release-notes/rl-2111.section.md b/nixos/doc/manual/release-notes/rl-2111.section.md index 3e1922ddcc2e..ebc200c4ad52 100644 --- a/nixos/doc/manual/release-notes/rl-2111.section.md +++ b/nixos/doc/manual/release-notes/rl-2111.section.md @@ -390,6 +390,16 @@ In addition to numerous new and upgraded packages, this release has the followin `myhostname`, but before `dns` should use the default priority - NSS modules which should come after `dns` should use mkAfter. +- The [networking.wireless](options.html#opt-networking.wireless.enable) module (based on wpa_supplicant) has been heavily reworked, solving a number of issues and adding useful features: + - The automatic discovery of wireless interfaces at boot has been made reliable again (issues [#101963](https://github.com/NixOS/nixpkgs/issues/101963), [#23196](https://github.com/NixOS/nixpkgs/issues/23196)). + - WPA3 and Fast BSS Transition (802.11r) are now enabled by default for all networks. + - Secrets like pre-shared keys and passwords can now be handled safely, meaning without including them in a world-readable file (`wpa_supplicant.conf` under /nix/store). + This is achieved by storing the secrets in a secured [environmentFile](options.html#opt-networking.wireless.environmentFile) and referring to them though environment variables that are expanded inside the configuration. + - With multiple interfaces declared, independent wpa_supplicant daemons are started, one for each interface (the services are named `wpa_supplicant-wlan0`, `wpa_supplicant-wlan1`, etc.). + - The generated `wpa_supplicant.conf` file is now formatted for easier reading. + - A new [scanOnLowSignal](options.html#opt-networking.wireless.scanOnLowSignal) option has been added to facilitate fast roaming between access points (enabled by default). + - A new [networks.<name>.authProtocols](options.html#opt-networking.wireless.networks._name_.authProtocols) option has been added to change the authentication protocols used when connecting to a network. + - The [networking.wireless.iwd](options.html#opt-networking.wireless.iwd.enable) module has a new [networking.wireless.iwd.settings](options.html#opt-networking.wireless.iwd.settings) option. - The [services.syncoid.enable](options.html#opt-services.syncoid.enable) module now properly drops ZFS permissions after usage. Before it delegated permissions to whole pools instead of datasets and didn't clean up after execution. You can manually look this up for your pools by running `zfs allow your-pool-name` and use `zfs unallow syncoid your-pool-name` to clean this up. diff --git a/nixos/modules/services/networking/wpa_supplicant.nix b/nixos/modules/services/networking/wpa_supplicant.nix index 155c6fdd0ab0..904a3db493b7 100644 --- a/nixos/modules/services/networking/wpa_supplicant.nix +++ b/nixos/modules/services/networking/wpa_supplicant.nix @@ -20,10 +20,16 @@ let ++ optional cfg.scanOnLowSignal ''bgscan="simple:30:-70:3600"'' ++ optional (cfg.extraConfig != "") cfg.extraConfig); + configIsGenerated = with cfg; + networks != {} || extraConfig != "" || userControlled.enable; + + # the original configuration file configFile = - if cfg.networks != {} || cfg.extraConfig != "" || cfg.userControlled.enable + if configIsGenerated then pkgs.writeText "wpa_supplicant.conf" generatedConfig else "/etc/wpa_supplicant.conf"; + # the config file with environment variables replaced + finalConfig = ''"$RUNTIME_DIRECTORY"/wpa_supplicant.conf''; # Creates a network block for wpa_supplicant.conf mkNetwork = ssid: opts: @@ -56,8 +62,8 @@ let let deviceUnit = optional (iface != null) "sys-subsystem-net-devices-${utils.escapeSystemdPath iface}.device"; configStr = if cfg.allowAuxiliaryImperativeNetworks - then "-c /etc/wpa_supplicant.conf -I ${configFile}" - else "-c ${configFile}"; + then "-c /etc/wpa_supplicant.conf -I ${finalConfig}" + else "-c ${finalConfig}"; in { description = "WPA Supplicant instance" + optionalString (iface != null) " for interface ${iface}"; @@ -69,12 +75,25 @@ let stopIfChanged = false; path = [ package ]; + serviceConfig.RuntimeDirectory = "wpa_supplicant"; + serviceConfig.RuntimeDirectoryMode = "700"; + serviceConfig.EnvironmentFile = mkIf (cfg.environmentFile != null) + (builtins.toString cfg.environmentFile); script = '' - if [ -f /etc/wpa_supplicant.conf -a "/etc/wpa_supplicant.conf" != "${configFile}" ]; then - echo >&2 "<3>/etc/wpa_supplicant.conf present but ignored. Generated ${configFile} is used instead." - fi + ${optionalString configIsGenerated '' + if [ -f /etc/wpa_supplicant.conf ]; then + echo >&2 "<3>/etc/wpa_supplicant.conf present but ignored. Generated ${configFile} is used instead." + fi + ''} + + # substitute environment variables + ${pkgs.gawk}/bin/awk '{ + for(varname in ENVIRON) + gsub("@"varname"@", ENVIRON[varname]) + print + }' "${configFile}" > "${finalConfig}" iface_args="-s ${optionalString cfg.dbusControlled "-u"} -D${cfg.driver} ${configStr}" @@ -155,6 +174,44 @@ in { ''; }; + environmentFile = mkOption { + type = types.nullOr types.path; + default = null; + example = "/run/secrets/wireless.env"; + description = '' + File consisting of lines of the form varname=value + to define variables for the wireless configuration. + + See section "EnvironmentFile=" in + systemd.exec5 + for a syntax reference. + + Secrets (PSKs, passwords, etc.) can be provided without adding them to + the world-readable Nix store by defining them in the environment file and + referring to them in option + with the syntax @varname@. Example: + + + # content of /run/secrets/wireless.env + PSK_HOME=mypassword + PASS_WORK=myworkpassword + + + + # wireless-related configuration + networking.wireless.environmentFile = "/run/secrets/wireless.env"; + networking.wireless.networks = { + home.psk = "@PSK_HOME@"; + work.auth = ''' + eap=PEAP + identity="my-user@example.com" + password="@PASS_WORK@" + '''; + }; + + ''; + }; + networks = mkOption { type = types.attrsOf (types.submodule { options = { @@ -165,10 +222,14 @@ in { The network's pre-shared key in plaintext defaulting to being a network without any authentication. - Be aware that these will be written to the nix store - in plaintext! + + Be aware that this will be written to the nix store + in plaintext! Use an environment variable instead. + - Mutually exclusive with pskRaw. + + Mutually exclusive with pskRaw. + ''; }; @@ -179,7 +240,14 @@ in { The network's pre-shared key in hex defaulting to being a network without any authentication. - Mutually exclusive with psk. + + Be aware that this will be written to the nix store + in plaintext! Use an environment variable instead. + + + + Mutually exclusive with psk. + ''; }; @@ -231,7 +299,7 @@ in { example = '' eap=PEAP identity="user@example.com" - password="secret" + password="@EXAMPLE_PASSWORD@" ''; description = '' Use this option to configure advanced authentication methods like EAP. @@ -242,7 +310,15 @@ in { for example configurations. - Mutually exclusive with psk and pskRaw. + + Be aware that this will be written to the nix store + in plaintext! Use an environment variable for secrets. + + + + Mutually exclusive with psk and + pskRaw. + ''; }; @@ -303,11 +379,17 @@ in { default = {}; example = literalExample '' { echelon = { # SSID with no spaces or special characters - psk = "abcdefgh"; + psk = "abcdefgh"; # (password will be written to /nix/store!) }; + + echelon = { # safe version of the above: read PSK from the + psk = "@PSK_ECHELON@"; # variable PSK_ECHELON, defined in environmentFile, + }; # this won't leak into /nix/store + "echelon's AP" = { # SSID with spaces and/or special characters - psk = "ijklmnop"; + psk = "ijklmnop"; # (password will be written to /nix/store!) }; + "free.wifi" = {}; # Public wireless network } ''; diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 0328727cc39c..f92a9241c506 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -479,6 +479,7 @@ in wiki-js = handleTest ./wiki-js.nix {}; wireguard = handleTest ./wireguard {}; wmderland = handleTest ./wmderland.nix {}; + wpa_supplicant = handleTest ./wpa_supplicant.nix {}; wordpress = handleTest ./wordpress.nix {}; xandikos = handleTest ./xandikos.nix {}; xautolock = handleTest ./xautolock.nix {}; diff --git a/nixos/tests/wpa_supplicant.nix b/nixos/tests/wpa_supplicant.nix new file mode 100644 index 000000000000..1d669d5016a7 --- /dev/null +++ b/nixos/tests/wpa_supplicant.nix @@ -0,0 +1,81 @@ +import ./make-test-python.nix ({ pkgs, lib, ...}: +{ + name = "wpa_supplicant"; + meta = with lib.maintainers; { + maintainers = [ rnhmjoj ]; + }; + + machine = { ... }: { + imports = [ ../modules/profiles/minimal.nix ]; + + # add a virtual wlan interface + boot.kernelModules = [ "mac80211_hwsim" ]; + + # wireless access point + services.hostapd = { + enable = true; + wpa = true; + interface = "wlan0"; + ssid = "nixos-test"; + wpaPassphrase = "reproducibility"; + }; + + # wireless client + networking.wireless = { + # the override is needed because the wifi is + # disabled with mkVMOverride in qemu-vm.nix. + enable = lib.mkOverride 0 true; + userControlled.enable = true; + interfaces = [ "wlan1" ]; + + networks = { + # test network + nixos-test.psk = "@PSK_NIXOS_TEST@"; + + # secrets substitution test cases + test1.psk = "@PSK_VALID@"; # should be replaced + test2.psk = "@PSK_SPECIAL@"; # should be replaced + test3.psk = "@PSK_MISSING@"; # should not be replaced + test4.psk = "P@ssowrdWithSome@tSymbol"; # should not be replaced + }; + + # secrets + environmentFile = pkgs.writeText "wpa-secrets" '' + PSK_NIXOS_TEST="reproducibility" + PSK_VALID="S0m3BadP4ssw0rd"; + # taken from https://github.com/minimaxir/big-list-of-naughty-strings + PSK_SPECIAL=",./;'[]\-= <>?:\"{}|_+ !@#$%^\&*()`~"; + ''; + }; + + }; + + testScript = + '' + config_file = "/run/wpa_supplicant/wpa_supplicant.conf" + + with subtest("Configuration file is inaccessible to other users"): + machine.wait_for_file(config_file) + machine.fail(f"sudo -u nobody ls {config_file}") + + with subtest("Secrets variables have been substituted"): + machine.fail(f"grep -q @PSK_VALID@ {config_file}") + machine.fail(f"grep -q @PSK_SPECIAL@ {config_file}") + machine.succeed(f"grep -q @PSK_MISSING@ {config_file}") + machine.succeed(f"grep -q P@ssowrdWithSome@tSymbol {config_file}") + + # save file for manual inspection + machine.copy_from_vm(config_file) + + with subtest("Daemon is running and accepting connections"): + machine.wait_for_unit("wpa_supplicant-wlan1.service") + status = machine.succeed("wpa_cli -i wlan1 status") + assert "Failed to connect" not in status, \ + "Failed to connect to the daemon" + + with subtest("Daemon can connect to the access point"): + machine.wait_until_succeeds( + "wpa_cli -i wlan1 status | grep -q wpa_state=COMPLETED" + ) + ''; +}) diff --git a/pkgs/os-specific/linux/wpa_supplicant/default.nix b/pkgs/os-specific/linux/wpa_supplicant/default.nix index 1dbe281e0967..656fa477768a 100644 --- a/pkgs/os-specific/linux/wpa_supplicant/default.nix +++ b/pkgs/os-specific/linux/wpa_supplicant/default.nix @@ -1,4 +1,5 @@ { lib, stdenv, fetchurl, fetchpatch, openssl, pkg-config, libnl +, nixosTests , withDbus ? true, dbus , withReadline ? true, readline , withPcsclite ? true, pcsclite @@ -139,6 +140,10 @@ stdenv.mkDerivation rec { install -Dm444 wpa_supplicant.conf $out/share/doc/wpa_supplicant/wpa_supplicant.conf.example ''; + passthru.tests = { + inherit (nixosTests) wpa_supplicant; + }; + meta = with lib; { homepage = "https://w1.fi/wpa_supplicant/"; description = "A tool for connecting to WPA and WPA2-protected wireless networks";