1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6
7 # The splicing information needed for nativeBuildInputs isn't available
8 # on the derivations likely to be used as `cfgc.package`.
9 # This middle-ground solution ensures *an* sshd can do their basic validation
10 # on the configuration.
11 validationPackage = if pkgs.stdenv.buildPlatform == pkgs.stdenv.hostPlatform
12 then cfgc.package
13 else pkgs.buildPackages.openssh;
14
15 # dont use the "=" operator
16 settingsFormat =
17 let
18 # reports boolean as yes / no
19 mkValueString = with lib; v:
20 if isInt v then toString v
21 else if isString v then v
22 else if true == v then "yes"
23 else if false == v then "no"
24 else throw "unsupported type ${builtins.typeOf v}: ${(lib.generators.toPretty {}) v}";
25
26 base = pkgs.formats.keyValue {
27 mkKeyValue = lib.generators.mkKeyValueDefault { inherit mkValueString; } " ";
28 };
29 # OpenSSH is very inconsistent with options that can take multiple values.
30 # For some of them, they can simply appear multiple times and are appended, for others the
31 # values must be separated by whitespace or even commas.
32 # Consult either sshd_config(5) or, as last resort, the OpehSSH source for parsing
33 # the options at servconf.c:process_server_config_line_depth() to determine the right "mode"
34 # for each. But fortunaly this fact is documented for most of them in the manpage.
35 commaSeparated = [ "Ciphers" "KexAlgorithms" "Macs" ];
36 spaceSeparated = [ "AuthorizedKeysFile" "AllowGroups" "AllowUsers" "DenyGroups" "DenyUsers" ];
37 in {
38 inherit (base) type;
39 generate = name: value:
40 let transformedValue = mapAttrs (key: val:
41 if isList val then
42 if elem key commaSeparated then concatStringsSep "," val
43 else if elem key spaceSeparated then concatStringsSep " " val
44 else throw "list value for unknown key ${key}: ${(lib.generators.toPretty {}) val}"
45 else
46 val
47 ) value;
48 in
49 base.generate name transformedValue;
50 };
51
52 configFile = settingsFormat.generate "sshd.conf-settings" (filterAttrs (n: v: v != null) cfg.settings);
53 sshconf = pkgs.runCommand "sshd.conf-final" { } ''
54 cat ${configFile} - >$out <<EOL
55 ${cfg.extraConfig}
56 EOL
57 '';
58
59 cfg = config.services.openssh;
60 cfgc = config.programs.ssh;
61
62
63 nssModulesPath = config.system.nssModules.path;
64
65 userOptions = {
66
67 options.openssh.authorizedKeys = {
68 keys = mkOption {
69 type = types.listOf types.singleLineStr;
70 default = [];
71 description = ''
72 A list of verbatim OpenSSH public keys that should be added to the
73 user's authorized keys. The keys are added to a file that the SSH
74 daemon reads in addition to the the user's authorized_keys file.
75 You can combine the `keys` and
76 `keyFiles` options.
77 Warning: If you are using `NixOps` then don't use this
78 option since it will replace the key required for deployment via ssh.
79 '';
80 example = [
81 "ssh-rsa AAAAB3NzaC1yc2etc/etc/etcjwrsh8e596z6J0l7 example@host"
82 "ssh-ed25519 AAAAC3NzaCetcetera/etceteraJZMfk3QPfQ foo@bar"
83 ];
84 };
85
86 keyFiles = mkOption {
87 type = types.listOf types.path;
88 default = [];
89 description = ''
90 A list of files each containing one OpenSSH public key that should be
91 added to the user's authorized keys. The contents of the files are
92 read at build time and added to a file that the SSH daemon reads in
93 addition to the the user's authorized_keys file. You can combine the
94 `keyFiles` and `keys` options.
95 '';
96 };
97 };
98
99 options.openssh.authorizedPrincipals = mkOption {
100 type = with types; listOf types.singleLineStr;
101 default = [];
102 description = ''
103 A list of verbatim principal names that should be added to the user's
104 authorized principals.
105 '';
106 example = [
107 "example@host"
108 "foo@bar"
109 ];
110 };
111
112 };
113
114 authKeysFiles = let
115 mkAuthKeyFile = u: nameValuePair "ssh/authorized_keys.d/${u.name}" {
116 mode = "0444";
117 source = pkgs.writeText "${u.name}-authorized_keys" ''
118 ${concatStringsSep "\n" u.openssh.authorizedKeys.keys}
119 ${concatMapStrings (f: readFile f + "\n") u.openssh.authorizedKeys.keyFiles}
120 '';
121 };
122 usersWithKeys = attrValues (flip filterAttrs config.users.users (n: u:
123 length u.openssh.authorizedKeys.keys != 0 || length u.openssh.authorizedKeys.keyFiles != 0
124 ));
125 in listToAttrs (map mkAuthKeyFile usersWithKeys);
126
127 authPrincipalsFiles = let
128 mkAuthPrincipalsFile = u: nameValuePair "ssh/authorized_principals.d/${u.name}" {
129 mode = "0444";
130 text = concatStringsSep "\n" u.openssh.authorizedPrincipals;
131 };
132 usersWithPrincipals = attrValues (flip filterAttrs config.users.users (n: u:
133 length u.openssh.authorizedPrincipals != 0
134 ));
135 in listToAttrs (map mkAuthPrincipalsFile usersWithPrincipals);
136
137in
138
139{
140 imports = [
141 (mkAliasOptionModuleMD [ "services" "sshd" "enable" ] [ "services" "openssh" "enable" ])
142 (mkAliasOptionModuleMD [ "services" "openssh" "knownHosts" ] [ "programs" "ssh" "knownHosts" ])
143 (mkRenamedOptionModule [ "services" "openssh" "challengeResponseAuthentication" ] [ "services" "openssh" "kbdInteractiveAuthentication" ])
144
145 (mkRenamedOptionModule [ "services" "openssh" "kbdInteractiveAuthentication" ] [ "services" "openssh" "settings" "KbdInteractiveAuthentication" ])
146 (mkRenamedOptionModule [ "services" "openssh" "passwordAuthentication" ] [ "services" "openssh" "settings" "PasswordAuthentication" ])
147 (mkRenamedOptionModule [ "services" "openssh" "useDns" ] [ "services" "openssh" "settings" "UseDns" ])
148 (mkRenamedOptionModule [ "services" "openssh" "permitRootLogin" ] [ "services" "openssh" "settings" "PermitRootLogin" ])
149 (mkRenamedOptionModule [ "services" "openssh" "logLevel" ] [ "services" "openssh" "settings" "LogLevel" ])
150 (mkRenamedOptionModule [ "services" "openssh" "macs" ] [ "services" "openssh" "settings" "Macs" ])
151 (mkRenamedOptionModule [ "services" "openssh" "ciphers" ] [ "services" "openssh" "settings" "Ciphers" ])
152 (mkRenamedOptionModule [ "services" "openssh" "kexAlgorithms" ] [ "services" "openssh" "settings" "KexAlgorithms" ])
153 (mkRenamedOptionModule [ "services" "openssh" "gatewayPorts" ] [ "services" "openssh" "settings" "GatewayPorts" ])
154 (mkRenamedOptionModule [ "services" "openssh" "forwardX11" ] [ "services" "openssh" "settings" "X11Forwarding" ])
155 ];
156
157 ###### interface
158
159 options = {
160
161 services.openssh = {
162
163 enable = mkOption {
164 type = types.bool;
165 default = false;
166 description = ''
167 Whether to enable the OpenSSH secure shell daemon, which
168 allows secure remote logins.
169 '';
170 };
171
172 startWhenNeeded = mkOption {
173 type = types.bool;
174 default = false;
175 description = ''
176 If set, {command}`sshd` is socket-activated; that
177 is, instead of having it permanently running as a daemon,
178 systemd will start an instance for each incoming connection.
179 '';
180 };
181
182 allowSFTP = mkOption {
183 type = types.bool;
184 default = true;
185 description = ''
186 Whether to enable the SFTP subsystem in the SSH daemon. This
187 enables the use of commands such as {command}`sftp` and
188 {command}`sshfs`.
189 '';
190 };
191
192 sftpServerExecutable = mkOption {
193 type = types.str;
194 example = "internal-sftp";
195 description = ''
196 The sftp server executable. Can be a path or "internal-sftp" to use
197 the sftp server built into the sshd binary.
198 '';
199 };
200
201 sftpFlags = mkOption {
202 type = with types; listOf str;
203 default = [];
204 example = [ "-f AUTHPRIV" "-l INFO" ];
205 description = ''
206 Commandline flags to add to sftp-server.
207 '';
208 };
209
210 ports = mkOption {
211 type = types.listOf types.port;
212 default = [22];
213 description = ''
214 Specifies on which ports the SSH daemon listens.
215 '';
216 };
217
218 openFirewall = mkOption {
219 type = types.bool;
220 default = true;
221 description = ''
222 Whether to automatically open the specified ports in the firewall.
223 '';
224 };
225
226 listenAddresses = mkOption {
227 type = with types; listOf (submodule {
228 options = {
229 addr = mkOption {
230 type = types.nullOr types.str;
231 default = null;
232 description = ''
233 Host, IPv4 or IPv6 address to listen to.
234 '';
235 };
236 port = mkOption {
237 type = types.nullOr types.int;
238 default = null;
239 description = ''
240 Port to listen to.
241 '';
242 };
243 };
244 });
245 default = [];
246 example = [ { addr = "192.168.3.1"; port = 22; } { addr = "0.0.0.0"; port = 64022; } ];
247 description = ''
248 List of addresses and ports to listen on (ListenAddress directive
249 in config). If port is not specified for address sshd will listen
250 on all ports specified by `ports` option.
251 NOTE: this will override default listening on all local addresses and port 22.
252 NOTE: setting this option won't automatically enable given ports
253 in firewall configuration.
254 '';
255 };
256
257 hostKeys = mkOption {
258 type = types.listOf types.attrs;
259 default =
260 [ { type = "rsa"; bits = 4096; path = "/etc/ssh/ssh_host_rsa_key"; }
261 { type = "ed25519"; path = "/etc/ssh/ssh_host_ed25519_key"; }
262 ];
263 example =
264 [ { type = "rsa"; bits = 4096; path = "/etc/ssh/ssh_host_rsa_key"; rounds = 100; openSSHFormat = true; }
265 { type = "ed25519"; path = "/etc/ssh/ssh_host_ed25519_key"; rounds = 100; comment = "key comment"; }
266 ];
267 description = ''
268 NixOS can automatically generate SSH host keys. This option
269 specifies the path, type and size of each key. See
270 {manpage}`ssh-keygen(1)` for supported types
271 and sizes.
272 '';
273 };
274
275 banner = mkOption {
276 type = types.nullOr types.lines;
277 default = null;
278 description = ''
279 Message to display to the remote user before authentication is allowed.
280 '';
281 };
282
283 authorizedKeysFiles = mkOption {
284 type = types.listOf types.str;
285 default = [];
286 description = ''
287 Specify the rules for which files to read on the host.
288
289 This is an advanced option. If you're looking to configure user
290 keys, you can generally use [](#opt-users.users._name_.openssh.authorizedKeys.keys)
291 or [](#opt-users.users._name_.openssh.authorizedKeys.keyFiles).
292
293 These are paths relative to the host root file system or home
294 directories and they are subject to certain token expansion rules.
295 See AuthorizedKeysFile in man sshd_config for details.
296 '';
297 };
298
299 authorizedKeysInHomedir = mkOption {
300 type = types.bool;
301 default = true;
302 description = ''
303 Enables the use of the `~/.ssh/authorized_keys` file.
304
305 Otherwise, the only files trusted by default are those in `/etc/ssh/authorized_keys.d`,
306 *i.e.* SSH keys from [](#opt-users.users._name_.openssh.authorizedKeys.keys).
307 '';
308 };
309
310 authorizedKeysCommand = mkOption {
311 type = types.str;
312 default = "none";
313 description = ''
314 Specifies a program to be used to look up the user's public
315 keys. The program must be owned by root, not writable by group
316 or others and specified by an absolute path.
317 '';
318 };
319
320 authorizedKeysCommandUser = mkOption {
321 type = types.str;
322 default = "nobody";
323 description = ''
324 Specifies the user under whose account the AuthorizedKeysCommand
325 is run. It is recommended to use a dedicated user that has no
326 other role on the host than running authorized keys commands.
327 '';
328 };
329
330
331
332 settings = mkOption {
333 description = "Configuration for `sshd_config(5)`.";
334 default = { };
335 example = literalExpression ''
336 {
337 UseDns = true;
338 PasswordAuthentication = false;
339 }
340 '';
341 type = types.submodule ({name, ...}: {
342 freeformType = settingsFormat.type;
343 options = {
344 AuthorizedPrincipalsFile = mkOption {
345 type = types.str;
346 default = "none"; # upstream default
347 description = ''
348 Specifies a file that lists principal names that are accepted for certificate authentication. The default
349 is `"none"`, i.e. not to use a principals file.
350 '';
351 };
352 LogLevel = mkOption {
353 type = types.enum [ "QUIET" "FATAL" "ERROR" "INFO" "VERBOSE" "DEBUG" "DEBUG1" "DEBUG2" "DEBUG3" ];
354 default = "INFO"; # upstream default
355 description = ''
356 Gives the verbosity level that is used when logging messages from sshd(8). Logging with a DEBUG level
357 violates the privacy of users and is not recommended.
358 '';
359 };
360 UsePAM = mkEnableOption "PAM authentication" // { default = true; };
361 UseDns = mkOption {
362 type = types.bool;
363 # apply if cfg.useDns then "yes" else "no"
364 default = false;
365 description = ''
366 Specifies whether sshd(8) should look up the remote host name, and to check that the resolved host name for
367 the remote IP address maps back to the very same IP address.
368 If this option is set to no (the default) then only addresses and not host names may be used in
369 ~/.ssh/authorized_keys from and sshd_config Match Host directives.
370 '';
371 };
372 X11Forwarding = mkOption {
373 type = types.bool;
374 default = false;
375 description = ''
376 Whether to allow X11 connections to be forwarded.
377 '';
378 };
379 PasswordAuthentication = mkOption {
380 type = types.bool;
381 default = true;
382 description = ''
383 Specifies whether password authentication is allowed.
384 '';
385 };
386 PermitRootLogin = mkOption {
387 default = "prohibit-password";
388 type = types.enum ["yes" "without-password" "prohibit-password" "forced-commands-only" "no"];
389 description = ''
390 Whether the root user can login using ssh.
391 '';
392 };
393 KbdInteractiveAuthentication = mkOption {
394 type = types.bool;
395 default = true;
396 description = ''
397 Specifies whether keyboard-interactive authentication is allowed.
398 '';
399 };
400 GatewayPorts = mkOption {
401 type = types.str;
402 default = "no";
403 description = ''
404 Specifies whether remote hosts are allowed to connect to
405 ports forwarded for the client. See
406 {manpage}`sshd_config(5)`.
407 '';
408 };
409 KexAlgorithms = mkOption {
410 type = types.listOf types.str;
411 default = [
412 "sntrup761x25519-sha512@openssh.com"
413 "curve25519-sha256"
414 "curve25519-sha256@libssh.org"
415 "diffie-hellman-group-exchange-sha256"
416 ];
417 description = ''
418 Allowed key exchange algorithms
419
420 Uses the lower bound recommended in both
421 <https://stribika.github.io/2015/01/04/secure-secure-shell.html>
422 and
423 <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67>
424 '';
425 };
426 Macs = mkOption {
427 type = types.listOf types.str;
428 default = [
429 "hmac-sha2-512-etm@openssh.com"
430 "hmac-sha2-256-etm@openssh.com"
431 "umac-128-etm@openssh.com"
432 ];
433 description = ''
434 Allowed MACs
435
436 Defaults to recommended settings from both
437 <https://stribika.github.io/2015/01/04/secure-secure-shell.html>
438 and
439 <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67>
440 '';
441 };
442 StrictModes = mkOption {
443 type = types.bool;
444 default = true;
445 description = ''
446 Whether sshd should check file modes and ownership of directories
447 '';
448 };
449 Ciphers = mkOption {
450 type = types.listOf types.str;
451 default = [
452 "chacha20-poly1305@openssh.com"
453 "aes256-gcm@openssh.com"
454 "aes128-gcm@openssh.com"
455 "aes256-ctr"
456 "aes192-ctr"
457 "aes128-ctr"
458 ];
459 description = ''
460 Allowed ciphers
461
462 Defaults to recommended settings from both
463 <https://stribika.github.io/2015/01/04/secure-secure-shell.html>
464 and
465 <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67>
466 '';
467 };
468 AllowUsers = mkOption {
469 type = with types; nullOr (listOf str);
470 default = null;
471 description = ''
472 If specified, login is allowed only for the listed users.
473 See {manpage}`sshd_config(5)` for details.
474 '';
475 };
476 DenyUsers = mkOption {
477 type = with types; nullOr (listOf str);
478 default = null;
479 description = ''
480 If specified, login is denied for all listed users. Takes
481 precedence over [](#opt-services.openssh.settings.AllowUsers).
482 See {manpage}`sshd_config(5)` for details.
483 '';
484 };
485 AllowGroups = mkOption {
486 type = with types; nullOr (listOf str);
487 default = null;
488 description = ''
489 If specified, login is allowed only for users part of the
490 listed groups.
491 See {manpage}`sshd_config(5)` for details.
492 '';
493 };
494 DenyGroups = mkOption {
495 type = with types; nullOr (listOf str);
496 default = null;
497 description = ''
498 If specified, login is denied for all users part of the listed
499 groups. Takes precedence over
500 [](#opt-services.openssh.settings.AllowGroups). See
501 {manpage}`sshd_config(5)` for details.
502 '';
503 };
504 # Disabled by default, since pam_motd handles this.
505 PrintMotd = mkEnableOption "printing /etc/motd when a user logs in interactively";
506 };
507 });
508 };
509
510 extraConfig = mkOption {
511 type = types.lines;
512 default = "";
513 description = "Verbatim contents of {file}`sshd_config`.";
514 };
515
516 moduliFile = mkOption {
517 example = "/etc/my-local-ssh-moduli;";
518 type = types.path;
519 description = ''
520 Path to `moduli` file to install in
521 `/etc/ssh/moduli`. If this option is unset, then
522 the `moduli` file shipped with OpenSSH will be used.
523 '';
524 };
525
526 };
527
528 users.users = mkOption {
529 type = with types; attrsOf (submodule userOptions);
530 };
531
532 };
533
534
535 ###### implementation
536
537 config = mkIf cfg.enable {
538
539 users.users.sshd =
540 {
541 isSystemUser = true;
542 group = "sshd";
543 description = "SSH privilege separation user";
544 };
545 users.groups.sshd = {};
546
547 services.openssh.moduliFile = mkDefault "${cfgc.package}/etc/ssh/moduli";
548 services.openssh.sftpServerExecutable = mkDefault "${cfgc.package}/libexec/sftp-server";
549
550 environment.etc = authKeysFiles // authPrincipalsFiles //
551 { "ssh/moduli".source = cfg.moduliFile;
552 "ssh/sshd_config".source = sshconf;
553 };
554
555 systemd =
556 let
557 service =
558 { description = "SSH Daemon";
559 wantedBy = optional (!cfg.startWhenNeeded) "multi-user.target";
560 after = [ "network.target" ];
561 stopIfChanged = false;
562 path = [ cfgc.package pkgs.gawk ];
563 environment.LD_LIBRARY_PATH = nssModulesPath;
564
565 restartTriggers = optionals (!cfg.startWhenNeeded) [
566 config.environment.etc."ssh/sshd_config".source
567 ];
568
569 preStart =
570 ''
571 # Make sure we don't write to stdout, since in case of
572 # socket activation, it goes to the remote side (#19589).
573 exec >&2
574
575 ${flip concatMapStrings cfg.hostKeys (k: ''
576 if ! [ -s "${k.path}" ]; then
577 if ! [ -h "${k.path}" ]; then
578 rm -f "${k.path}"
579 fi
580 mkdir -m 0755 -p "$(dirname '${k.path}')"
581 ssh-keygen \
582 -t "${k.type}" \
583 ${optionalString (k ? bits) "-b ${toString k.bits}"} \
584 ${optionalString (k ? rounds) "-a ${toString k.rounds}"} \
585 ${optionalString (k ? comment) "-C '${k.comment}'"} \
586 ${optionalString (k ? openSSHFormat && k.openSSHFormat) "-o"} \
587 -f "${k.path}" \
588 -N ""
589 fi
590 '')}
591 '';
592
593 serviceConfig =
594 { ExecStart =
595 (optionalString cfg.startWhenNeeded "-") +
596 "${cfgc.package}/bin/sshd " + (optionalString cfg.startWhenNeeded "-i ") +
597 "-D " + # don't detach into a daemon process
598 "-f /etc/ssh/sshd_config";
599 KillMode = "process";
600 } // (if cfg.startWhenNeeded then {
601 StandardInput = "socket";
602 StandardError = "journal";
603 } else {
604 Restart = "always";
605 Type = "simple";
606 });
607
608 };
609 in
610
611 if cfg.startWhenNeeded then {
612
613 sockets.sshd =
614 { description = "SSH Socket";
615 wantedBy = [ "sockets.target" ];
616 socketConfig.ListenStream = if cfg.listenAddresses != [] then
617 concatMap
618 ({ addr, port }:
619 if port != null then [ "${addr}:${toString port}" ]
620 else map (p: "${addr}:${toString p}") cfg.ports)
621 cfg.listenAddresses
622 else
623 cfg.ports;
624 socketConfig.Accept = true;
625 # Prevent brute-force attacks from shutting down socket
626 socketConfig.TriggerLimitIntervalSec = 0;
627 };
628
629 services."sshd@" = service;
630
631 } else {
632
633 services.sshd = service;
634
635 };
636
637 networking.firewall.allowedTCPPorts = optionals cfg.openFirewall cfg.ports;
638
639 security.pam.services.sshd = lib.mkIf cfg.settings.UsePAM
640 { startSession = true;
641 showMotd = true;
642 unixAuth = cfg.settings.PasswordAuthentication;
643 };
644
645 # These values are merged with the ones defined externally, see:
646 # https://github.com/NixOS/nixpkgs/pull/10155
647 # https://github.com/NixOS/nixpkgs/pull/41745
648 services.openssh.authorizedKeysFiles =
649 lib.optional cfg.authorizedKeysInHomedir "%h/.ssh/authorized_keys" ++ [ "/etc/ssh/authorized_keys.d/%u" ];
650
651 services.openssh.settings.AuthorizedPrincipalsFile = mkIf (authPrincipalsFiles != {}) "/etc/ssh/authorized_principals.d/%u";
652
653 services.openssh.extraConfig = mkOrder 0
654 ''
655 Banner ${if cfg.banner == null then "none" else pkgs.writeText "ssh_banner" cfg.banner}
656
657 AddressFamily ${if config.networking.enableIPv6 then "any" else "inet"}
658 ${concatMapStrings (port: ''
659 Port ${toString port}
660 '') cfg.ports}
661
662 ${concatMapStrings ({ port, addr, ... }: ''
663 ListenAddress ${addr}${optionalString (port != null) (":" + toString port)}
664 '') cfg.listenAddresses}
665
666 ${optionalString cfgc.setXAuthLocation ''
667 XAuthLocation ${pkgs.xorg.xauth}/bin/xauth
668 ''}
669 ${optionalString cfg.allowSFTP ''
670 Subsystem sftp ${cfg.sftpServerExecutable} ${concatStringsSep " " cfg.sftpFlags}
671 ''}
672 AuthorizedKeysFile ${toString cfg.authorizedKeysFiles}
673 ${optionalString (cfg.authorizedKeysCommand != "none") ''
674 AuthorizedKeysCommand ${cfg.authorizedKeysCommand}
675 AuthorizedKeysCommandUser ${cfg.authorizedKeysCommandUser}
676 ''}
677
678 ${flip concatMapStrings cfg.hostKeys (k: ''
679 HostKey ${k.path}
680 '')}
681 '';
682
683 system.checks = [
684 (pkgs.runCommand "check-sshd-config"
685 {
686 nativeBuildInputs = [ validationPackage ];
687 } ''
688 ${concatMapStringsSep "\n"
689 (lport: "sshd -G -T -C lport=${toString lport} -f ${sshconf} > /dev/null")
690 cfg.ports}
691 ${concatMapStringsSep "\n"
692 (la:
693 concatMapStringsSep "\n"
694 (port: "sshd -G -T -C ${escapeShellArg "laddr=${la.addr},lport=${toString port}"} -f ${sshconf} > /dev/null")
695 (if la.port != null then [ la.port ] else cfg.ports)
696 )
697 cfg.listenAddresses}
698 touch $out
699 '')
700 ];
701
702 assertions = [{ assertion = if cfg.settings.X11Forwarding then cfgc.setXAuthLocation else true;
703 message = "cannot enable X11 forwarding without setting xauth location";}
704 (let
705 duplicates =
706 # Filter out the groups with more than 1 element
707 lib.filter (l: lib.length l > 1) (
708 # Grab the groups, we don't care about the group identifiers
709 lib.attrValues (
710 # Group the settings that are the same in lower case
711 lib.groupBy lib.strings.toLower (attrNames cfg.settings)
712 )
713 );
714 formattedDuplicates = lib.concatMapStringsSep ", " (dupl: "(${lib.concatStringsSep ", " dupl})") duplicates;
715 in
716 {
717 assertion = lib.length duplicates == 0;
718 message = ''Duplicate sshd config key; does your capitalization match the option's? Duplicate keys: ${formattedDuplicates}'';
719 })]
720 ++ forEach cfg.listenAddresses ({ addr, ... }: {
721 assertion = addr != null;
722 message = "addr must be specified in each listenAddresses entry";
723 });
724 };
725
726}