Merge pull request #227442 from christoph-heiss/openssh/allowusers

openssh: add {Allow,Deny}{Users,Groups} settings
This commit is contained in:
Aaron Andersen 2023-10-29 08:20:22 -04:00 committed by GitHub
commit 3b848391b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 103 additions and 14 deletions

View file

@ -12,22 +12,44 @@ let
then cfgc.package
else pkgs.buildPackages.openssh;
# reports boolean as yes / no
mkValueStringSshd = 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;
} " ";});
settingsFormat =
let
# reports boolean as yes / no
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 throw "unsupported type ${builtins.typeOf v}: ${(lib.generators.toPretty {}) v}";
configFile = settingsFormat.generate "sshd.conf-settings" cfg.settings;
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" (filterAttrs (n: v: v != null) cfg.settings);
sshconf = pkgs.runCommand "sshd.conf-final" { } ''
cat ${configFile} - >$out <<EOL
${cfg.extraConfig}
@ -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.
'';
};
};
});
};

View file

@ -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 =
{ ... }: { };
@ -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
)
'';
})