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