Merge pull request #134618 from rnhmjoj/wpa-safe

nixos/wpa_supplicant: add safe secret handling
This commit is contained in:
Michele Guerini Rocco 2021-09-29 13:35:29 +02:00 committed by GitHub
commit e68eba2dba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 260 additions and 14 deletions

View file

@ -1277,6 +1277,73 @@ Superuser created successfully.
</listitem>
</itemizedlist>
</listitem>
<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

View file

@ -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.&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.

View file

@ -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 <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 = {
@ -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!
<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>
'';
};
@ -179,7 +240,14 @@ in {
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>
'';
};
@ -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 {
</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>
'';
};
@ -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
}
'';

View file

@ -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 {};

View file

@ -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"
)
'';
})

View file

@ -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";