Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nixos/sshd: implement support for Match groups #56345

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
235 changes: 152 additions & 83 deletions nixos/modules/services/networking/ssh/sshd.nix
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ let

nssModulesPath = config.system.nssModules.path;

yesNo = x: if x then "yes" else "no";

indent = c: x:
let
indentStr = concatStrings (builtins.genList (_: " ") c);
in
removeSuffix indentStr (concatStringsSep "\n" (flip map (splitString "\n" x)
(n: "${optionalString (n != "") indentStr}${n}")));

userOptions = {

options.openssh.authorizedKeys = {
Expand Down Expand Up @@ -54,6 +63,86 @@ let
));
in listToAttrs (map mkAuthKeyFile usersWithKeys);

# options that can be used globally or in a `Match` declaration
generalOptions = {
authorizedKeysFiles = mkOption {
type = types.listOf types.str;
default = [];
description = "Files from which authorized keys are read.";
};

gatewayPorts = mkOption {
type = types.str;
default = "no";
description = ''
Specifies whether remote hosts are allowed to connect to
ports forwarded for the client. See
<citerefentry><refentrytitle>sshd_config</refentrytitle>
<manvolnum>5</manvolnum></citerefentry>.
'';
};

logLevel = mkOption {
type = types.enum [ "QUIET" "FATAL" "ERROR" "INFO" "VERBOSE" "DEBUG" "DEBUG1" "DEBUG2" "DEBUG3" ];
default = "VERBOSE";
description = ''
Gives the verbosity level that is used when logging messages from sshd(8). The possible values are:
QUIET, FATAL, ERROR, INFO, VERBOSE, DEBUG, DEBUG1, DEBUG2, and DEBUG3. The default is VERBOSE. DEBUG and DEBUG1
are equivalent. DEBUG2 and DEBUG3 each specify higher levels of debugging output. Logging with a DEBUG level
violates the privacy of users and is not recommended.

LogLevel VERBOSE logs user's key fingerprint on login.
Needed to have a clear audit track of which key was used to log in.
'';
};

passwordAuthentication = mkOption {
type = types.bool;
default = true;
description = ''
Specifies whether password authentication is allowed.
'';
};

permitRootLogin = mkOption {
default = "prohibit-password";
type = types.enum ["yes" "without-password" "prohibit-password" "forced-commands-only" "no"];
description = ''
Whether the root user can login using ssh.
'';
};

forwardX11 = mkOption {
type = types.bool;
default = false;
description = ''
Whether to allow X11 connections to be forwarded.
'';
};

extraConfig = mkOption {
type = types.lines;
default = "";
description = "Verbatim contents of <filename>sshd_config</filename>.";
};
};

renderGeneralOptions = cfg: ''
${optionalString cfgc.setXAuthLocation ''
XAuthLocation ${pkgs.xorg.xauth}/bin/xauth
''}

X11Forwarding ${yesNo cfg.forwardX11}

AuthorizedKeysFile ${toString cfg.authorizedKeysFiles}

PermitRootLogin ${cfg.permitRootLogin}
GatewayPorts ${cfg.gatewayPorts}
PasswordAuthentication ${yesNo cfg.passwordAuthentication}

LogLevel ${cfg.logLevel}
'';

in

{
Expand All @@ -62,7 +151,7 @@ in

options = {

services.openssh = {
services.openssh = generalOptions // {

enable = mkOption {
type = types.bool;
Expand All @@ -83,14 +172,6 @@ in
'';
};

forwardX11 = mkOption {
type = types.bool;
default = false;
description = ''
Whether to allow X11 connections to be forwarded.
'';
};

allowSFTP = mkOption {
type = types.bool;
default = true;
Expand All @@ -110,25 +191,6 @@ in
'';
};

permitRootLogin = mkOption {
default = "prohibit-password";
type = types.enum ["yes" "without-password" "prohibit-password" "forced-commands-only" "no"];
description = ''
Whether the root user can login using ssh.
'';
};

gatewayPorts = mkOption {
type = types.str;
default = "no";
description = ''
Specifies whether remote hosts are allowed to connect to
ports forwarded for the client. See
<citerefentry><refentrytitle>sshd_config</refentrytitle>
<manvolnum>5</manvolnum></citerefentry>.
'';
};

ports = mkOption {
type = types.listOf types.port;
default = [22];
Expand All @@ -145,6 +207,52 @@ in
'';
};

matches = mkOption {
default = [];
example = literalExample ''
[
{
match = {
"10.23.42.0/24" = "Address";
"somebody" = "User";
};
config = {
forwardX11 = true;
permitRootLogin = "yes";
};
}
{
match."10.23.42.0/24" = "Address";
config.logLevel = "ERROR";
}
]
'';

description = ''
Declaratively specify matches like <literal>Match Address 10.23.42.0/24</literal>.
'';

type = types.listOf (types.submodule {
options = {
match = mkOption {
type = types.attrsOf (types.enum [ "Address" "User" ]);
description = ''
Configure matches for openssh.
'';

apply = x: concatStringsSep " " (mapAttrsToList (match: type: "${type} ${match}") x);
};

config = mkOption {
type = types.submodule ({ options = generalOptions; });
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably want to merge the values here with the configs from services.openssh. So if somebody sets services.openssh.permitRootLogin to no, each Match group should also have permitRootLogin set to no.

But I'll wait for some reviews from other folks, before spending more time on this :)

description = ''
The options that only apply if the match is true.
'';
};
};
});
};

listenAddresses = mkOption {
type = with types; listOf (submodule {
options = {
Expand Down Expand Up @@ -176,14 +284,6 @@ in
'';
};

passwordAuthentication = mkOption {
type = types.bool;
default = true;
description = ''
Specifies whether password authentication is allowed.
'';
};

challengeResponseAuthentication = mkOption {
type = types.bool;
default = true;
Expand Down Expand Up @@ -211,12 +311,6 @@ in
'';
};

authorizedKeysFiles = mkOption {
type = types.listOf types.str;
default = [];
description = "Files from which authorized keys are read.";
};

kexAlgorithms = mkOption {
type = types.listOf types.str;
default = [
Expand Down Expand Up @@ -276,20 +370,6 @@ in
'';
};

logLevel = mkOption {
type = types.enum [ "QUIET" "FATAL" "ERROR" "INFO" "VERBOSE" "DEBUG" "DEBUG1" "DEBUG2" "DEBUG3" ];
default = "VERBOSE";
description = ''
Gives the verbosity level that is used when logging messages from sshd(8). The possible values are:
QUIET, FATAL, ERROR, INFO, VERBOSE, DEBUG, DEBUG1, DEBUG2, and DEBUG3. The default is VERBOSE. DEBUG and DEBUG1
are equivalent. DEBUG2 and DEBUG3 each specify higher levels of debugging output. Logging with a DEBUG level
violates the privacy of users and is not recommended.

LogLevel VERBOSE logs user's key fingerprint on login.
Needed to have a clear audit track of which key was used to log in.
'';
};

useDns = mkOption {
type = types.bool;
default = false;
Expand All @@ -301,12 +381,6 @@ in
'';
};

extraConfig = mkOption {
type = types.lines;
default = "";
description = "Verbatim contents of <filename>sshd_config</filename>.";
};

moduliFile = mkOption {
example = "/etc/my-local-ssh-moduli;";
type = types.path;
Expand All @@ -328,7 +402,7 @@ in

###### implementation

config = mkIf cfg.enable {
config = mkIf cfg.enable (mkMerge [{

users.users.sshd =
{ isSystemUser = true;
Expand Down Expand Up @@ -435,6 +509,8 @@ in

UsePAM yes

${renderGeneralOptions cfg}

AddressFamily ${if config.networking.enableIPv6 then "any" else "inet"}
${concatMapStrings (port: ''
Port ${toString port}
Expand All @@ -444,29 +520,14 @@ in
ListenAddress ${addr}${if port != null then ":" + toString port else ""}
'') cfg.listenAddresses}

${optionalString cfgc.setXAuthLocation ''
XAuthLocation ${pkgs.xorg.xauth}/bin/xauth
''}

${if cfg.forwardX11 then ''
X11Forwarding yes
'' else ''
X11Forwarding no
''}

${optionalString cfg.allowSFTP ''
Subsystem sftp ${cfgc.package}/libexec/sftp-server ${concatStringsSep " " cfg.sftpFlags}
''}

PermitRootLogin ${cfg.permitRootLogin}
GatewayPorts ${cfg.gatewayPorts}
PasswordAuthentication ${if cfg.passwordAuthentication then "yes" else "no"}
ChallengeResponseAuthentication ${if cfg.challengeResponseAuthentication then "yes" else "no"}
ChallengeResponseAuthentication ${yesNo cfg.challengeResponseAuthentication}

PrintMotd no # handled by pam_motd

AuthorizedKeysFile ${toString cfg.authorizedKeysFiles}

${flip concatMapStrings cfg.hostKeys (k: ''
HostKey ${k.path}
'')}
Expand All @@ -475,14 +536,11 @@ in
Ciphers ${concatStringsSep "," cfg.ciphers}
MACs ${concatStringsSep "," cfg.macs}

LogLevel ${cfg.logLevel}

${if cfg.useDns then ''
UseDNS yes
'' else ''
UseDNS no
''}

'';

assertions = [{ assertion = if cfg.forwardX11 then cfgc.setXAuthLocation else true;
Expand All @@ -492,6 +550,17 @@ in
message = "addr must be specified in each listenAddresses entry";
});

};
} (mkIf (cfg.matches != {}) {

# position matches at the very last position in the config.
# Otherwise all following options would be only used if the option applies.
services.openssh.extraConfig = mkOrder 9999
(concatMapStringsSep "\n" (match: ''
Match ${match.match}
${indent 2 (renderGeneralOptions match.config)}
${indent 2 match.config.extraConfig}
'') cfg.matches);

})]);

}
Loading