Loading nixos/modules/services/networking/ssh/sshd.nix +72 −14 Original line number Diff line number Diff line Loading @@ -12,22 +12,44 @@ let then cfgc.package else pkgs.buildPackages.openssh; # dont use the "=" operator settingsFormat = let # reports boolean as yes / no mkValueStringSshd = with lib; v: mkValueString = with lib; v: if isInt v then toString v else if isString v then v else if true == v then "yes" else if false == v then "no" else if isList v then concatStringsSep "," v else throw "unsupported type ${builtins.typeOf v}: ${(lib.generators.toPretty {}) v}"; # dont use the "=" operator settingsFormat = (pkgs.formats.keyValue { mkKeyValue = lib.generators.mkKeyValueDefault { mkValueString = mkValueStringSshd; } " ";}); base = pkgs.formats.keyValue { mkKeyValue = lib.generators.mkKeyValueDefault { inherit mkValueString; } " "; }; # OpenSSH is very inconsistent with options that can take multiple values. # For some of them, they can simply appear multiple times and are appended, for others the # values must be separated by whitespace or even commas. # Consult either sshd_config(5) or, as last resort, the OpehSSH source for parsing # the options at servconf.c:process_server_config_line_depth() to determine the right "mode" # for each. But fortunaly this fact is documented for most of them in the manpage. commaSeparated = [ "Ciphers" "KexAlgorithms" "Macs" ]; spaceSeparated = [ "AuthorizedKeysFile" "AllowGroups" "AllowUsers" "DenyGroups" "DenyUsers" ]; in { inherit (base) type; generate = name: value: let transformedValue = mapAttrs (key: val: if isList val then if elem key commaSeparated then concatStringsSep "," val else if elem key spaceSeparated then concatStringsSep " " val else throw "list value for unknown key ${key}: ${(lib.generators.toPretty {}) val}" else val ) value; in base.generate name transformedValue; }; configFile = settingsFormat.generate "sshd.conf-settings" cfg.settings; configFile = settingsFormat.generate "sshd.conf-settings" (filterAttrs (n: v: v != null) cfg.settings); sshconf = pkgs.runCommand "sshd.conf-final" { } '' cat ${configFile} - >$out <<EOL ${cfg.extraConfig} Loading Loading @@ -431,6 +453,42 @@ in <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67> ''; }; AllowUsers = mkOption { type = with types; nullOr (listOf str); default = null; description = lib.mdDoc '' If specified, login is allowed only for the listed users. See {manpage}`sshd_config(5)` for details. ''; }; DenyUsers = mkOption { type = with types; nullOr (listOf str); default = null; description = lib.mdDoc '' If specified, login is denied for all listed users. Takes precedence over [](#opt-services.openssh.settings.AllowUsers). See {manpage}`sshd_config(5)` for details. ''; }; AllowGroups = mkOption { type = with types; nullOr (listOf str); default = null; description = lib.mdDoc '' If specified, login is allowed only for users part of the listed groups. See {manpage}`sshd_config(5)` for details. ''; }; DenyGroups = mkOption { type = with types; nullOr (listOf str); default = null; description = lib.mdDoc '' If specified, login is denied for all users part of the listed groups. Takes precedence over [](#opt-services.openssh.settings.AllowGroups). See {manpage}`sshd_config(5)` for details. ''; }; }; }); }; Loading nixos/tests/openssh.nix +31 −0 Original line number Diff line number Diff line Loading @@ -82,6 +82,19 @@ in { }; }; server_allowedusers = { ... }: { services.openssh = { enable = true; settings.AllowUsers = [ "alice" "bob" ]; }; users.groups = { alice = { }; bob = { }; carol = { }; }; users.users = { alice = { isNormalUser = true; group = "alice"; openssh.authorizedKeys.keys = [ snakeOilPublicKey ]; }; bob = { isNormalUser = true; group = "bob"; openssh.authorizedKeys.keys = [ snakeOilPublicKey ]; }; carol = { isNormalUser = true; group = "carol"; openssh.authorizedKeys.keys = [ snakeOilPublicKey ]; }; }; }; client = { ... }: { }; Loading Loading @@ -147,5 +160,23 @@ in { with subtest("match-rules"): server_match_rule.succeed("ss -nlt | grep '127.0.0.1:22'") with subtest("allowed-users"): client.succeed( "cat ${snakeOilPrivateKey} > privkey.snakeoil" ) client.succeed("chmod 600 privkey.snakeoil") client.succeed( "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil alice@server_allowedusers true", timeout=30 ) client.succeed( "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil bob@server_allowedusers true", timeout=30 ) client.fail( "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil carol@server_allowedusers true", timeout=30 ) ''; }) Loading
nixos/modules/services/networking/ssh/sshd.nix +72 −14 Original line number Diff line number Diff line Loading @@ -12,22 +12,44 @@ let then cfgc.package else pkgs.buildPackages.openssh; # dont use the "=" operator settingsFormat = let # reports boolean as yes / no mkValueStringSshd = with lib; v: mkValueString = with lib; v: if isInt v then toString v else if isString v then v else if true == v then "yes" else if false == v then "no" else if isList v then concatStringsSep "," v else throw "unsupported type ${builtins.typeOf v}: ${(lib.generators.toPretty {}) v}"; # dont use the "=" operator settingsFormat = (pkgs.formats.keyValue { mkKeyValue = lib.generators.mkKeyValueDefault { mkValueString = mkValueStringSshd; } " ";}); base = pkgs.formats.keyValue { mkKeyValue = lib.generators.mkKeyValueDefault { inherit mkValueString; } " "; }; # OpenSSH is very inconsistent with options that can take multiple values. # For some of them, they can simply appear multiple times and are appended, for others the # values must be separated by whitespace or even commas. # Consult either sshd_config(5) or, as last resort, the OpehSSH source for parsing # the options at servconf.c:process_server_config_line_depth() to determine the right "mode" # for each. But fortunaly this fact is documented for most of them in the manpage. commaSeparated = [ "Ciphers" "KexAlgorithms" "Macs" ]; spaceSeparated = [ "AuthorizedKeysFile" "AllowGroups" "AllowUsers" "DenyGroups" "DenyUsers" ]; in { inherit (base) type; generate = name: value: let transformedValue = mapAttrs (key: val: if isList val then if elem key commaSeparated then concatStringsSep "," val else if elem key spaceSeparated then concatStringsSep " " val else throw "list value for unknown key ${key}: ${(lib.generators.toPretty {}) val}" else val ) value; in base.generate name transformedValue; }; configFile = settingsFormat.generate "sshd.conf-settings" cfg.settings; configFile = settingsFormat.generate "sshd.conf-settings" (filterAttrs (n: v: v != null) cfg.settings); sshconf = pkgs.runCommand "sshd.conf-final" { } '' cat ${configFile} - >$out <<EOL ${cfg.extraConfig} Loading Loading @@ -431,6 +453,42 @@ in <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67> ''; }; AllowUsers = mkOption { type = with types; nullOr (listOf str); default = null; description = lib.mdDoc '' If specified, login is allowed only for the listed users. See {manpage}`sshd_config(5)` for details. ''; }; DenyUsers = mkOption { type = with types; nullOr (listOf str); default = null; description = lib.mdDoc '' If specified, login is denied for all listed users. Takes precedence over [](#opt-services.openssh.settings.AllowUsers). See {manpage}`sshd_config(5)` for details. ''; }; AllowGroups = mkOption { type = with types; nullOr (listOf str); default = null; description = lib.mdDoc '' If specified, login is allowed only for users part of the listed groups. See {manpage}`sshd_config(5)` for details. ''; }; DenyGroups = mkOption { type = with types; nullOr (listOf str); default = null; description = lib.mdDoc '' If specified, login is denied for all users part of the listed groups. Takes precedence over [](#opt-services.openssh.settings.AllowGroups). See {manpage}`sshd_config(5)` for details. ''; }; }; }); }; Loading
nixos/tests/openssh.nix +31 −0 Original line number Diff line number Diff line Loading @@ -82,6 +82,19 @@ in { }; }; server_allowedusers = { ... }: { services.openssh = { enable = true; settings.AllowUsers = [ "alice" "bob" ]; }; users.groups = { alice = { }; bob = { }; carol = { }; }; users.users = { alice = { isNormalUser = true; group = "alice"; openssh.authorizedKeys.keys = [ snakeOilPublicKey ]; }; bob = { isNormalUser = true; group = "bob"; openssh.authorizedKeys.keys = [ snakeOilPublicKey ]; }; carol = { isNormalUser = true; group = "carol"; openssh.authorizedKeys.keys = [ snakeOilPublicKey ]; }; }; }; client = { ... }: { }; Loading Loading @@ -147,5 +160,23 @@ in { with subtest("match-rules"): server_match_rule.succeed("ss -nlt | grep '127.0.0.1:22'") with subtest("allowed-users"): client.succeed( "cat ${snakeOilPrivateKey} > privkey.snakeoil" ) client.succeed("chmod 600 privkey.snakeoil") client.succeed( "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil alice@server_allowedusers true", timeout=30 ) client.succeed( "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil bob@server_allowedusers true", timeout=30 ) client.fail( "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i privkey.snakeoil carol@server_allowedusers true", timeout=30 ) ''; })