Unverified Commit 3b848391 authored by Aaron Andersen's avatar Aaron Andersen Committed by GitHub
Browse files

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

openssh: add {Allow,Deny}{Users,Groups} settings
parents f4bc9a6e cc8ba216
Loading
Loading
Loading
Loading
+72 −14
Original line number Diff line number Diff line
@@ -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}
@@ -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.
              '';
            };
          };
        });
      };
+31 −0
Original line number Diff line number Diff line
@@ -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
        )
  '';
})