mirror of
https://github.com/NixOS/nixpkgs.git
synced 2025-07-13 21:50:33 +03:00
nixos/wireless: reimplement secrets using ext_password_backend
This replaces the current implementation (splicing the secrets into the configuration file using environment variables) with the new built-in mechanism ext_password_backend. With some minor syntax changes, it works exactly as before, except the heavy lifting is done by wpa_supplicant and probably less error-prone.
This commit is contained in:
parent
c1c478cd72
commit
f235dda87f
2 changed files with 89 additions and 99 deletions
|
@ -45,6 +45,8 @@ let
|
||||||
"update_config=1"
|
"update_config=1"
|
||||||
])
|
])
|
||||||
++ [ "pmf=1" ]
|
++ [ "pmf=1" ]
|
||||||
|
++ optional (cfg.secretsFile != null)
|
||||||
|
"ext_password_backend=file:${cfg.secretsFile}"
|
||||||
++ optional cfg.scanOnLowSignal ''bgscan="simple:30:-70:3600"''
|
++ optional cfg.scanOnLowSignal ''bgscan="simple:30:-70:3600"''
|
||||||
++ optional (cfg.extraConfig != "") cfg.extraConfig);
|
++ optional (cfg.extraConfig != "") cfg.extraConfig);
|
||||||
|
|
||||||
|
@ -56,8 +58,6 @@ let
|
||||||
if configIsGenerated
|
if configIsGenerated
|
||||||
then pkgs.writeText "wpa_supplicant.conf" generatedConfig
|
then pkgs.writeText "wpa_supplicant.conf" generatedConfig
|
||||||
else "/etc/wpa_supplicant.conf";
|
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
|
# Creates a network block for wpa_supplicant.conf
|
||||||
mkNetwork = opts:
|
mkNetwork = opts:
|
||||||
|
@ -90,8 +90,8 @@ let
|
||||||
let
|
let
|
||||||
deviceUnit = optional (iface != null) "sys-subsystem-net-devices-${utils.escapeSystemdPath iface}.device";
|
deviceUnit = optional (iface != null) "sys-subsystem-net-devices-${utils.escapeSystemdPath iface}.device";
|
||||||
configStr = if cfg.allowAuxiliaryImperativeNetworks
|
configStr = if cfg.allowAuxiliaryImperativeNetworks
|
||||||
then "-c /etc/wpa_supplicant.conf -I ${finalConfig}"
|
then "-c /etc/wpa_supplicant.conf -I ${configFile}"
|
||||||
else "-c ${finalConfig}";
|
else "-c ${configFile}";
|
||||||
in {
|
in {
|
||||||
description = "WPA Supplicant instance" + optionalString (iface != null) " for interface ${iface}";
|
description = "WPA Supplicant instance" + optionalString (iface != null) " for interface ${iface}";
|
||||||
|
|
||||||
|
@ -109,8 +109,6 @@ let
|
||||||
serviceConfig.UMask = "066";
|
serviceConfig.UMask = "066";
|
||||||
serviceConfig.RuntimeDirectory = "wpa_supplicant";
|
serviceConfig.RuntimeDirectory = "wpa_supplicant";
|
||||||
serviceConfig.RuntimeDirectoryMode = "700";
|
serviceConfig.RuntimeDirectoryMode = "700";
|
||||||
serviceConfig.EnvironmentFile = mkIf (cfg.environmentFile != null)
|
|
||||||
(builtins.toString cfg.environmentFile);
|
|
||||||
|
|
||||||
script =
|
script =
|
||||||
''
|
''
|
||||||
|
@ -125,21 +123,6 @@ let
|
||||||
touch /etc/wpa_supplicant.conf
|
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}"
|
iface_args="-s ${optionalString cfg.dbusControlled "-u"} -D${cfg.driver} ${configStr}"
|
||||||
|
|
||||||
${if iface == null then ''
|
${if iface == null then ''
|
||||||
|
@ -231,36 +214,34 @@ in {
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
environmentFile = mkOption {
|
secretsFile = mkOption {
|
||||||
type = types.nullOr types.path;
|
type = types.nullOr types.path;
|
||||||
default = null;
|
default = null;
|
||||||
example = "/run/secrets/wireless.env";
|
example = "/run/secrets/wireless.conf";
|
||||||
description = ''
|
description = ''
|
||||||
File consisting of lines of the form `varname=value`
|
File consisting of lines of the form `varname=value`
|
||||||
to define variables for the wireless configuration.
|
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
|
Secrets (PSKs, passwords, etc.) can be provided without adding them to
|
||||||
the world-readable Nix store by defining them in the environment file and
|
the world-readable Nix store by defining them in the secrets file and
|
||||||
referring to them in option {option}`networking.wireless.networks`
|
referring to them in option [](#opt-networking.wireless.networks)
|
||||||
with the syntax `@varname@`. Example:
|
with the syntax `ext:secretname`. Example:
|
||||||
|
|
||||||
```
|
```
|
||||||
# content of /run/secrets/wireless.env
|
# content of /run/secrets/wireless.conf
|
||||||
PSK_HOME=mypassword
|
psk_home=mypassword
|
||||||
PASS_WORK=myworkpassword
|
psk_other=6a381cea59c7a2d6b30736ba0e6f397f7564a044bcdb7a327a1d16a1ed91b327
|
||||||
```
|
pass_work=myworkpassword
|
||||||
|
|
||||||
```
|
|
||||||
# wireless-related configuration
|
# wireless-related configuration
|
||||||
networking.wireless.environmentFile = "/run/secrets/wireless.env";
|
networking.wireless.secretsFile = "/run/secrets/wireless.conf";
|
||||||
networking.wireless.networks = {
|
networking.wireless.networks = {
|
||||||
home.psk = "@PSK_HOME@";
|
home.pskRaw = "ext:psk_home";
|
||||||
|
other.pskRaw = "ext:psk_other";
|
||||||
work.auth = '''
|
work.auth = '''
|
||||||
eap=PEAP
|
eap=PEAP
|
||||||
identity="my-user@example.com"
|
identity="my-user@example.com"
|
||||||
password="@PASS_WORK@"
|
password=ext:pass_work
|
||||||
''';
|
''';
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
@ -271,15 +252,16 @@ in {
|
||||||
type = types.attrsOf (types.submodule {
|
type = types.attrsOf (types.submodule {
|
||||||
options = {
|
options = {
|
||||||
psk = mkOption {
|
psk = mkOption {
|
||||||
type = types.nullOr types.str;
|
type = types.nullOr (types.strMatching "[[:print:]]{8,63}");
|
||||||
default = null;
|
default = null;
|
||||||
description = ''
|
description = ''
|
||||||
The network's pre-shared key in plaintext defaulting
|
The network's pre-shared key in plaintext defaulting
|
||||||
to being a network without any authentication.
|
to being a network without any authentication.
|
||||||
|
|
||||||
::: {.warning}
|
::: {.warning}
|
||||||
Be aware that this will be written to the nix store
|
Be aware that this will be written to the Nix store
|
||||||
in plaintext! Use an environment variable instead.
|
in plaintext! Use {var}`pskRaw` with an external
|
||||||
|
reference to keep it safe.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
::: {.note}
|
::: {.note}
|
||||||
|
@ -289,19 +271,28 @@ in {
|
||||||
};
|
};
|
||||||
|
|
||||||
pskRaw = mkOption {
|
pskRaw = mkOption {
|
||||||
type = types.nullOr types.str;
|
type = types.nullOr
|
||||||
|
(types.strMatching "([[:xdigit:]]{64})|(ext:[^=]+)");
|
||||||
default = null;
|
default = null;
|
||||||
|
example = "ext:name_of_the_secret_here";
|
||||||
description = ''
|
description = ''
|
||||||
The network's pre-shared key in hex defaulting
|
Either the raw pre-shared key in hexadecimal format
|
||||||
to being a network without any authentication.
|
or the name of the secret (as defined inside
|
||||||
|
[](#opt-networking.wireless.secretsFile) and prefixed
|
||||||
|
with `ext:`) containing the network pre-shared key.
|
||||||
|
|
||||||
::: {.warning}
|
::: {.warning}
|
||||||
Be aware that this will be written to the nix store
|
Be aware that this will be written to the Nix store
|
||||||
in plaintext! Use an environment variable instead.
|
in plaintext! Always use an external reference.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
::: {.note}
|
::: {.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 = ''
|
example = ''
|
||||||
eap=PEAP
|
eap=PEAP
|
||||||
identity="user@example.com"
|
identity="user@example.com"
|
||||||
password="@EXAMPLE_PASSWORD@"
|
password=ext:example_password
|
||||||
'';
|
'';
|
||||||
description = ''
|
description = ''
|
||||||
Use this option to configure advanced authentication methods like EAP.
|
Use this option to configure advanced authentication methods
|
||||||
See
|
like EAP. See {manpage}`wpa_supplicant.conf(5)` for example
|
||||||
{manpage}`wpa_supplicant.conf(5)`
|
configurations.
|
||||||
for example configurations.
|
|
||||||
|
|
||||||
::: {.warning}
|
::: {.warning}
|
||||||
Be aware that this will be written to the nix store
|
Be aware that this will be written to the Nix store
|
||||||
in plaintext! Use an environment variable for secrets.
|
in plaintext! Use an external reference like
|
||||||
|
`ext:secretname` for secrets.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
::: {.note}
|
::: {.note}
|
||||||
Mutually exclusive with {var}`psk` and
|
Mutually exclusive with {var}`psk` and {var}`pskRaw`.
|
||||||
{var}`pskRaw`.
|
|
||||||
:::
|
:::
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -393,13 +383,14 @@ in {
|
||||||
type = types.nullOr types.int;
|
type = types.nullOr types.int;
|
||||||
default = null;
|
default = null;
|
||||||
description = ''
|
description = ''
|
||||||
By default, all networks will get same priority group (0). If some of the
|
By default, all networks will get same priority group (0). If
|
||||||
networks are more desirable, this field can be used to change the order in
|
some of the networks are more desirable, this field can be used
|
||||||
which wpa_supplicant goes through the networks when selecting a BSS. The
|
to change the order in which wpa_supplicant goes through the
|
||||||
priority groups will be iterated in decreasing priority (i.e., the larger the
|
networks when selecting a BSS. The priority groups will be
|
||||||
priority value, the sooner the network is matched against the scan results).
|
iterated in decreasing priority (i.e., the larger the priority
|
||||||
Within each priority group, networks will be selected based on security
|
value, the sooner the network is matched against the scan
|
||||||
policy, signal strength, etc.
|
results). Within each priority group, networks will be selected
|
||||||
|
based on security policy, signal strength, etc.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -411,9 +402,7 @@ in {
|
||||||
'';
|
'';
|
||||||
description = ''
|
description = ''
|
||||||
Extra configuration lines appended to the network block.
|
Extra configuration lines appended to the network block.
|
||||||
See
|
See {manpage}`wpa_supplicant.conf(5)` for available options.
|
||||||
{manpage}`wpa_supplicant.conf(5)`
|
|
||||||
for available options.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -432,7 +421,7 @@ in {
|
||||||
};
|
};
|
||||||
|
|
||||||
echelon = { # safe version of the above: read PSK from the
|
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
|
}; # this won't leak into /nix/store
|
||||||
|
|
||||||
"echelon's AP" = { # SSID with spaces and/or special characters
|
"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.<name>.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 {
|
config = mkIf cfg.enable {
|
||||||
assertions = flip mapAttrsToList cfg.networks (name: cfg: {
|
assertions = flip mapAttrsToList cfg.networks (name: cfg: {
|
||||||
assertion = with cfg; count (x: x != null) [ psk pskRaw auth ] <= 1;
|
assertion = with cfg; count (x: x != null) [ psk pskRaw auth ] <= 1;
|
||||||
|
|
|
@ -65,8 +65,8 @@ import ./make-test-python.nix ({ pkgs, lib, ...}:
|
||||||
# network that should be tested
|
# network that should be tested
|
||||||
|
|
||||||
# secrets
|
# secrets
|
||||||
environmentFile = pkgs.writeText "wpa-secrets" ''
|
secretsFile = pkgs.writeText "wpa-secrets" ''
|
||||||
PSK_NIXOS_TEST="reproducibility"
|
psk_nixos_test="reproducibility"
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -96,22 +96,7 @@ import ./make-test-python.nix ({ pkgs, lib, ...}:
|
||||||
psk = "password";
|
psk = "password";
|
||||||
authProtocols = [ "SAE" ];
|
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 = {
|
networking.wireless = {
|
||||||
fallbackToWPA2 = false;
|
fallbackToWPA2 = false;
|
||||||
networks.nixos-test-sae = {
|
networks.nixos-test-sae = {
|
||||||
psk = "@PSK_NIXOS_TEST@";
|
pskRaw = "ext:psk_nixos_test";
|
||||||
authProtocols = [ "SAE" ];
|
authProtocols = [ "SAE" ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -146,7 +131,7 @@ import ./make-test-python.nix ({ pkgs, lib, ...}:
|
||||||
networking.wireless = {
|
networking.wireless = {
|
||||||
fallbackToWPA2 = false;
|
fallbackToWPA2 = false;
|
||||||
networks.nixos-test-mixed = {
|
networks.nixos-test-mixed = {
|
||||||
psk = "@PSK_NIXOS_TEST@";
|
pskRaw = "ext:psk_nixos_test";
|
||||||
authProtocols = [ "SAE" ];
|
authProtocols = [ "SAE" ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -157,7 +142,7 @@ import ./make-test-python.nix ({ pkgs, lib, ...}:
|
||||||
networking.wireless = {
|
networking.wireless = {
|
||||||
fallbackToWPA2 = true;
|
fallbackToWPA2 = true;
|
||||||
networks.nixos-test-mixed = {
|
networks.nixos-test-mixed = {
|
||||||
psk = "@PSK_NIXOS_TEST@";
|
pskRaw = "ext:psk_nixos_test";
|
||||||
authProtocols = [ "WPA-PSK-SHA256" ];
|
authProtocols = [ "WPA-PSK-SHA256" ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -168,7 +153,7 @@ import ./make-test-python.nix ({ pkgs, lib, ...}:
|
||||||
networking.wireless = {
|
networking.wireless = {
|
||||||
fallbackToWPA2 = true;
|
fallbackToWPA2 = true;
|
||||||
networks.nixos-test-wpa2 = {
|
networks.nixos-test-wpa2 = {
|
||||||
psk = "@PSK_NIXOS_TEST@";
|
pskRaw = "ext:psk_nixos_test";
|
||||||
authProtocols = [ "WPA-PSK-SHA256" ];
|
authProtocols = [ "WPA-PSK-SHA256" ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -177,18 +162,10 @@ import ./make-test-python.nix ({ pkgs, lib, ...}:
|
||||||
|
|
||||||
testScript =
|
testScript =
|
||||||
''
|
''
|
||||||
config_file = "/run/wpa_supplicant/wpa_supplicant.conf"
|
# get the configuration file
|
||||||
|
basic.wait_for_unit("wpa_supplicant-wlan1.service")
|
||||||
with subtest("Configuration file is inaccessible to other users"):
|
cmdline = basic.succeed("cat /proc/$(pgrep wpa)/cmdline").split('\x00')
|
||||||
basic.wait_for_file(config_file)
|
config_file = cmdline[cmdline.index("-c") + 1]
|
||||||
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}")
|
|
||||||
|
|
||||||
with subtest("WPA2 fallbacks have been generated"):
|
with subtest("WPA2 fallbacks have been generated"):
|
||||||
assert int(basic.succeed(f"grep -c sae-only {config_file}")) == 1
|
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"
|
"Failed to connect to the daemon"
|
||||||
|
|
||||||
with subtest("Daemon can be configured imperatively"):
|
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.wait_until_succeeds("wpa_cli -i wlan1 status")
|
||||||
imperative.succeed("wpa_cli -i wlan1 add_network")
|
imperative.succeed("wpa_cli -i wlan1 add_network")
|
||||||
imperative.succeed("wpa_cli -i wlan1 set_network 0 ssid '\"nixos-test\"'")
|
imperative.succeed("wpa_cli -i wlan1 set_network 0 ssid '\"nixos-test\"'")
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue