Merge pull request #134618 from rnhmjoj/wpa-safe

nixos/wpa_supplicant: add safe secret handling

Changed files
+260 -14
nixos
doc
manual
from_md
release-notes
release-notes
modules
services
networking
tests
pkgs
os-specific
linux
wpa_supplicant
+67
nixos/doc/manual/from_md/release-notes/rl-2111.section.xml
···
<listitem>
<para>
The
+
<link xlink:href="options.html#opt-networking.wireless.enable">networking.wireless</link>
+
module (based on wpa_supplicant) has been heavily reworked,
+
solving a number of issues and adding useful features:
+
</para>
+
<itemizedlist spacing="compact">
+
<listitem>
+
<para>
+
The automatic discovery of wireless interfaces at boot has
+
been made reliable again (issues
+
<link xlink:href="https://github.com/NixOS/nixpkgs/issues/101963">#101963</link>,
+
<link xlink:href="https://github.com/NixOS/nixpkgs/issues/23196">#23196</link>).
+
</para>
+
</listitem>
+
<listitem>
+
<para>
+
WPA3 and Fast BSS Transition (802.11r) are now enabled by
+
default for all networks.
+
</para>
+
</listitem>
+
<listitem>
+
<para>
+
Secrets like pre-shared keys and passwords can now be
+
handled safely, meaning without including them in a
+
world-readable file
+
(<literal>wpa_supplicant.conf</literal> under /nix/store).
+
This is achieved by storing the secrets in a secured
+
<link xlink:href="options.html#opt-networking.wireless.environmentFile">environmentFile</link>
+
and referring to them though environment variables that
+
are expanded inside the configuration.
+
</para>
+
</listitem>
+
<listitem>
+
<para>
+
With multiple interfaces declared, independent
+
wpa_supplicant daemons are started, one for each interface
+
(the services are named
+
<literal>wpa_supplicant-wlan0</literal>,
+
<literal>wpa_supplicant-wlan1</literal>, etc.).
+
</para>
+
</listitem>
+
<listitem>
+
<para>
+
The generated <literal>wpa_supplicant.conf</literal> file
+
is now formatted for easier reading.
+
</para>
+
</listitem>
+
<listitem>
+
<para>
+
A new
+
<link xlink:href="options.html#opt-networking.wireless.scanOnLowSignal">scanOnLowSignal</link>
+
option has been added to facilitate fast roaming between
+
access points (enabled by default).
+
</para>
+
</listitem>
+
<listitem>
+
<para>
+
A new
+
<link xlink:href="options.html#opt-networking.wireless.networks._name_.authProtocols">networks.&lt;name&gt;.authProtocols</link>
+
option has been added to change the authentication
+
protocols used when connecting to a network.
+
</para>
+
</listitem>
+
</itemizedlist>
+
</listitem>
+
<listitem>
+
<para>
+
The
<link xlink:href="options.html#opt-networking.wireless.iwd.enable">networking.wireless.iwd</link>
module has a new
<link xlink:href="options.html#opt-networking.wireless.iwd.settings">networking.wireless.iwd.settings</link>
+10
nixos/doc/manual/release-notes/rl-2111.section.md
···
`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.&lt;name&gt;.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.
+96 -14
nixos/modules/services/networking/wpa_supplicant.nix
···
++ 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:
···
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}";
···
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}"
···
'';
};
+
environmentFile = mkOption {
+
type = types.nullOr types.path;
+
default = null;
+
example = "/run/secrets/wireless.env";
+
description = ''
+
File consisting of lines of the form <literal>varname=value</literal>
+
to define variables for the wireless configuration.
+
+
See section "EnvironmentFile=" in <citerefentry>
+
<refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum>
+
</citerefentry> 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</option>
+
with the syntax <literal>@varname@</literal>. Example:
+
+
<programlisting>
+
# content of /run/secrets/wireless.env
+
PSK_HOME=mypassword
+
PASS_WORK=myworkpassword
+
</programlisting>
+
+
<programlisting>
+
# 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@"
+
''';
+
};
+
</programlisting>
+
'';
+
};
+
networks = mkOption {
type = types.attrsOf (types.submodule {
options = {
···
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!
+
<warning><para>
+
Be aware that this will be written to the nix store
+
in plaintext! Use an environment variable instead.
+
</para></warning>
-
Mutually exclusive with <varname>pskRaw</varname>.
+
<note><para>
+
Mutually exclusive with <varname>pskRaw</varname>.
+
</para></note>
'';
};
···
The network's pre-shared key in hex defaulting
to being a network without any authentication.
-
Mutually exclusive with <varname>psk</varname>.
+
<warning><para>
+
Be aware that this will be written to the nix store
+
in plaintext! Use an environment variable instead.
+
</para></warning>
+
+
<note><para>
+
Mutually exclusive with <varname>psk</varname>.
+
</para></note>
'';
};
···
example = ''
eap=PEAP
identity="user@example.com"
-
password="secret"
+
password="@EXAMPLE_PASSWORD@"
'';
description = ''
Use this option to configure advanced authentication methods like EAP.
···
</citerefentry>
for example configurations.
-
Mutually exclusive with <varname>psk</varname> and <varname>pskRaw</varname>.
+
<warning><para>
+
Be aware that this will be written to the nix store
+
in plaintext! Use an environment variable for secrets.
+
</para></warning>
+
+
<note><para>
+
Mutually exclusive with <varname>psk</varname> and
+
<varname>pskRaw</varname>.
+
</para></note>
'';
};
···
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
}
'';
+1
nixos/tests/all-tests.nix
···
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 {};
+81
nixos/tests/wpa_supplicant.nix
···
+
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"
+
)
+
'';
+
})
+5
pkgs/os-specific/linux/wpa_supplicant/default.nix
···
{ lib, stdenv, fetchurl, fetchpatch, openssl, pkg-config, libnl
+
, nixosTests
, withDbus ? true, dbus
, withReadline ? true, readline
, withPcsclite ? true, pcsclite
···
rm $out/share/man/man8/wpa_priv.8
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/";