diff --git a/nixos/modules/services/networking/wpa_supplicant.nix b/nixos/modules/services/networking/wpa_supplicant.nix index b068f2227310..4fec023a2368 100644 --- a/nixos/modules/services/networking/wpa_supplicant.nix +++ b/nixos/modules/services/networking/wpa_supplicant.nix @@ -45,6 +45,8 @@ let "update_config=1" ]) ++ [ "pmf=1" ] + ++ optional (cfg.secretsFile != null) + "ext_password_backend=file:${cfg.secretsFile}" ++ optional cfg.scanOnLowSignal ''bgscan="simple:30:-70:3600"'' ++ optional (cfg.extraConfig != "") cfg.extraConfig); @@ -56,8 +58,6 @@ let 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 = opts: @@ -90,8 +90,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 ${finalConfig}" - else "-c ${finalConfig}"; + then "-c /etc/wpa_supplicant.conf -I ${configFile}" + else "-c ${configFile}"; in { description = "WPA Supplicant instance" + optionalString (iface != null) " for interface ${iface}"; @@ -109,8 +109,6 @@ let serviceConfig.UMask = "066"; serviceConfig.RuntimeDirectory = "wpa_supplicant"; serviceConfig.RuntimeDirectoryMode = "700"; - serviceConfig.EnvironmentFile = mkIf (cfg.environmentFile != null) - (builtins.toString cfg.environmentFile); script = '' @@ -125,21 +123,6 @@ let touch /etc/wpa_supplicant.conf ''} - # substitute environment variables - if [ -f "${configFile}" ]; then - ${pkgs.gawk}/bin/awk '{ - for(varname in ENVIRON) { - find = "@"varname"@" - repl = ENVIRON[varname] - if (i = index($0, find)) - $0 = substr($0, 1, i-1) repl substr($0, i+length(find)) - } - print - }' "${configFile}" > ${finalConfig} - else - touch ${finalConfig} - fi - iface_args="-s ${optionalString cfg.dbusControlled "-u"} -D${cfg.driver} ${configStr}" ${if iface == null then '' @@ -231,36 +214,34 @@ in { ''; }; - environmentFile = mkOption { + secretsFile = mkOption { type = types.nullOr types.path; default = null; - example = "/run/secrets/wireless.env"; + example = "/run/secrets/wireless.conf"; description = '' File consisting of lines of the form `varname=value` to define variables for the wireless configuration. - See section "EnvironmentFile=" in {manpage}`systemd.exec(5)` 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 {option}`networking.wireless.networks` - with the syntax `@varname@`. Example: + the world-readable Nix store by defining them in the secrets file and + referring to them in option [](#opt-networking.wireless.networks) + with the syntax `ext:secretname`. Example: ``` - # content of /run/secrets/wireless.env - PSK_HOME=mypassword - PASS_WORK=myworkpassword - ``` + # content of /run/secrets/wireless.conf + psk_home=mypassword + psk_other=6a381cea59c7a2d6b30736ba0e6f397f7564a044bcdb7a327a1d16a1ed91b327 + pass_work=myworkpassword - ``` # wireless-related configuration - networking.wireless.environmentFile = "/run/secrets/wireless.env"; + networking.wireless.secretsFile = "/run/secrets/wireless.conf"; networking.wireless.networks = { - home.psk = "@PSK_HOME@"; + home.pskRaw = "ext:psk_home"; + other.pskRaw = "ext:psk_other"; work.auth = ''' eap=PEAP identity="my-user@example.com" - password="@PASS_WORK@" + password=ext:pass_work '''; }; ``` @@ -271,15 +252,16 @@ in { type = types.attrsOf (types.submodule { options = { psk = mkOption { - type = types.nullOr types.str; + type = types.nullOr (types.strMatching "[[:print:]]{8,63}"); default = null; description = '' The network's pre-shared key in plaintext defaulting to being a network without any authentication. ::: {.warning} - Be aware that this will be written to the nix store - in plaintext! Use an environment variable instead. + Be aware that this will be written to the Nix store + in plaintext! Use {var}`pskRaw` with an external + reference to keep it safe. ::: ::: {.note} @@ -289,19 +271,28 @@ in { }; pskRaw = mkOption { - type = types.nullOr types.str; + type = types.nullOr + (types.strMatching "([[:xdigit:]]{64})|(ext:[^=]+)"); default = null; + example = "ext:name_of_the_secret_here"; description = '' - The network's pre-shared key in hex defaulting - to being a network without any authentication. + Either the raw pre-shared key in hexadecimal format + or the name of the secret (as defined inside + [](#opt-networking.wireless.secretsFile) and prefixed + with `ext:`) containing the network pre-shared key. ::: {.warning} - Be aware that this will be written to the nix store - in plaintext! Use an environment variable instead. + Be aware that this will be written to the Nix store + in plaintext! Always use an external reference. ::: ::: {.note} - Mutually exclusive with {var}`psk`. + The external secret can be either the plaintext + passphrase or the raw pre-shared key. + ::: + + ::: {.note} + Mutually exclusive with {var}`psk` and {var}`auth`. ::: ''; }; @@ -354,22 +345,21 @@ in { example = '' eap=PEAP identity="user@example.com" - password="@EXAMPLE_PASSWORD@" + password=ext:example_password ''; description = '' - Use this option to configure advanced authentication methods like EAP. - See - {manpage}`wpa_supplicant.conf(5)` - for example configurations. + Use this option to configure advanced authentication methods + like EAP. See {manpage}`wpa_supplicant.conf(5)` for example + configurations. ::: {.warning} - Be aware that this will be written to the nix store - in plaintext! Use an environment variable for secrets. + Be aware that this will be written to the Nix store + in plaintext! Use an external reference like + `ext:secretname` for secrets. ::: ::: {.note} - Mutually exclusive with {var}`psk` and - {var}`pskRaw`. + Mutually exclusive with {var}`psk` and {var}`pskRaw`. ::: ''; }; @@ -393,13 +383,14 @@ in { type = types.nullOr types.int; default = null; description = '' - By default, all networks will get same priority group (0). If some of the - networks are more desirable, this field can be used to change the order in - which wpa_supplicant goes through the networks when selecting a BSS. The - priority groups will be iterated in decreasing priority (i.e., the larger the - priority value, the sooner the network is matched against the scan results). - Within each priority group, networks will be selected based on security - policy, signal strength, etc. + By default, all networks will get same priority group (0). If + some of the networks are more desirable, this field can be used + to change the order in which wpa_supplicant goes through the + networks when selecting a BSS. The priority groups will be + iterated in decreasing priority (i.e., the larger the priority + value, the sooner the network is matched against the scan + results). Within each priority group, networks will be selected + based on security policy, signal strength, etc. ''; }; @@ -411,9 +402,7 @@ in { ''; description = '' Extra configuration lines appended to the network block. - See - {manpage}`wpa_supplicant.conf(5)` - for available options. + See {manpage}`wpa_supplicant.conf(5)` for available options. ''; }; @@ -432,7 +421,7 @@ in { }; echelon = { # safe version of the above: read PSK from the - psk = "@PSK_ECHELON@"; # variable PSK_ECHELON, defined in environmentFile, + pskRaw = "ext:psk_echelon"; # variable psk_echelon, defined in secretsFile, }; # this won't leak into /nix/store "echelon's AP" = { # SSID with spaces and/or special characters @@ -493,6 +482,31 @@ in { }; }; + imports = [ + (mkRemovedOptionModule [ "networking" "wireless" "environmentFile" ] + '' + Secrets are now handled by the `networking.wireless.secretsFile` and + `networking.wireless.networks..pskRaw` options. + The change is motivated by a mechanism recently added by wpa_supplicant + itself to separate secrets from configuration, making the previous + method obsolete. + + The syntax of the `secretsFile` is the same as before, except the + values are interpreted literally, unlike environment variables. + To update, remove quotes or character escapes, if necessary, and + apply the following changes to your configuration: + { + home.psk = "@psk_home@"; → home.pskRaw = "ext:psk_home"; + other.pskRaw = "@psk_other@"; → other.pskRaw = "ext:psk_other"; + work.auth = ''' + eap=PEAP + identity="my-user@example.com" + password=@pass_work@ → password=ext:pass_work + '''; + } + '') + ]; + config = mkIf cfg.enable { assertions = flip mapAttrsToList cfg.networks (name: cfg: { assertion = with cfg; count (x: x != null) [ psk pskRaw auth ] <= 1; diff --git a/nixos/tests/wpa_supplicant.nix b/nixos/tests/wpa_supplicant.nix index 5e3b39f27ecf..51b3396329fc 100644 --- a/nixos/tests/wpa_supplicant.nix +++ b/nixos/tests/wpa_supplicant.nix @@ -65,8 +65,8 @@ import ./make-test-python.nix ({ pkgs, lib, ...}: # network that should be tested # secrets - environmentFile = pkgs.writeText "wpa-secrets" '' - PSK_NIXOS_TEST="reproducibility" + secretsFile = pkgs.writeText "wpa-secrets" '' + psk_nixos_test="reproducibility" ''; }; }; @@ -96,22 +96,7 @@ import ./make-test-python.nix ({ pkgs, lib, ...}: psk = "password"; authProtocols = [ "SAE" ]; }; - - # 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 - test5.psk = "@PSK_AWK_REGEX@"; # should be replaced }; - - # secrets - environmentFile = pkgs.writeText "wpa-secrets" '' - PSK_VALID="S0m3BadP4ssw0rd"; - # taken from https://github.com/minimaxir/big-list-of-naughty-strings - PSK_SPECIAL=",./;'[]\/\-= <>?:\"{}|_+ !@#$%^&*()`~"; - PSK_AWK_REGEX="PassowrdWith&symbol"; - ''; }; }; @@ -135,7 +120,7 @@ import ./make-test-python.nix ({ pkgs, lib, ...}: networking.wireless = { fallbackToWPA2 = false; networks.nixos-test-sae = { - psk = "@PSK_NIXOS_TEST@"; + pskRaw = "ext:psk_nixos_test"; authProtocols = [ "SAE" ]; }; }; @@ -146,7 +131,7 @@ import ./make-test-python.nix ({ pkgs, lib, ...}: networking.wireless = { fallbackToWPA2 = false; networks.nixos-test-mixed = { - psk = "@PSK_NIXOS_TEST@"; + pskRaw = "ext:psk_nixos_test"; authProtocols = [ "SAE" ]; }; }; @@ -157,7 +142,7 @@ import ./make-test-python.nix ({ pkgs, lib, ...}: networking.wireless = { fallbackToWPA2 = true; networks.nixos-test-mixed = { - psk = "@PSK_NIXOS_TEST@"; + pskRaw = "ext:psk_nixos_test"; authProtocols = [ "WPA-PSK-SHA256" ]; }; }; @@ -168,7 +153,7 @@ import ./make-test-python.nix ({ pkgs, lib, ...}: networking.wireless = { fallbackToWPA2 = true; networks.nixos-test-wpa2 = { - psk = "@PSK_NIXOS_TEST@"; + pskRaw = "ext:psk_nixos_test"; authProtocols = [ "WPA-PSK-SHA256" ]; }; }; @@ -177,18 +162,10 @@ import ./make-test-python.nix ({ pkgs, lib, ...}: testScript = '' - config_file = "/run/wpa_supplicant/wpa_supplicant.conf" - - with subtest("Configuration file is inaccessible to other users"): - basic.wait_for_file(config_file) - basic.fail(f"sudo -u nobody ls {config_file}") - - with subtest("Secrets variables have been substituted"): - basic.fail(f"grep -q @PSK_VALID@ {config_file}") - basic.fail(f"grep -q @PSK_SPECIAL@ {config_file}") - basic.succeed(f"grep -q @PSK_MISSING@ {config_file}") - basic.succeed(f"grep -q P@ssowrdWithSome@tSymbol {config_file}") - basic.succeed(f"grep -q 'PassowrdWith&symbol' {config_file}") + # get the configuration file + basic.wait_for_unit("wpa_supplicant-wlan1.service") + cmdline = basic.succeed("cat /proc/$(pgrep wpa)/cmdline").split('\x00') + config_file = cmdline[cmdline.index("-c") + 1] with subtest("WPA2 fallbacks have been generated"): assert int(basic.succeed(f"grep -c sae-only {config_file}")) == 1 @@ -204,7 +181,6 @@ import ./make-test-python.nix ({ pkgs, lib, ...}: "Failed to connect to the daemon" with subtest("Daemon can be configured imperatively"): - imperative.wait_for_unit("wpa_supplicant-wlan1.service") imperative.wait_until_succeeds("wpa_cli -i wlan1 status") imperative.succeed("wpa_cli -i wlan1 add_network") imperative.succeed("wpa_cli -i wlan1 set_network 0 ssid '\"nixos-test\"'")