From 52b9dd7bf69f2b333bfd1a1e6cbb7d187471054b Mon Sep 17 00:00:00 2001 From: rnhmjoj Date: Tue, 17 Aug 2021 00:05:01 +0200 Subject: [PATCH 1/3] nixos/wpa_supplicant: add safe secret handling --- .../services/networking/wpa_supplicant.nix | 110 +++++++++++++++--- 1 file changed, 96 insertions(+), 14 deletions(-) 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 } ''; From 62126f8c155d58f7836b2c3873a40e4ad00cf46e Mon Sep 17 00:00:00 2001 From: rnhmjoj Date: Fri, 24 Sep 2021 13:25:16 +0200 Subject: [PATCH 2/3] nixos/tests/wpa_supplicant: init --- nixos/tests/all-tests.nix | 1 + nixos/tests/wpa_supplicant.nix | 81 +++++++++++++++++++ .../linux/wpa_supplicant/default.nix | 5 ++ 3 files changed, 87 insertions(+) create mode 100644 nixos/tests/wpa_supplicant.nix diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index b1c3ccf782d1..e5172a6cd347 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -473,6 +473,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"; From 3a0437d2b01d56702d06bb3e787f0d0f6bd0ae92 Mon Sep 17 00:00:00 2001 From: rnhmjoj Date: Sat, 25 Sep 2021 23:39:25 +0200 Subject: [PATCH 3/3] nixos/release-notes: document wpa_supplicant changes --- .../from_md/release-notes/rl-2111.section.xml | 67 +++++++++++++++++++ .../manual/release-notes/rl-2111.section.md | 10 +++ 2 files changed, 77 insertions(+) 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 f88be3918795..92b00a294786 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 @@ -855,6 +855,73 @@ + + + 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 b7cb31883f67..1f4f522ac68a 100644 --- a/nixos/doc/manual/release-notes/rl-2111.section.md +++ b/nixos/doc/manual/release-notes/rl-2111.section.md @@ -223,6 +223,16 @@ To be able to access the web UI this port needs to be opened in the firewall. `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.