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 fortunately 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.port;
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 openSSHFormat = true;
370 }
371 {
372 type = "ed25519";
373 path = "/etc/ssh/ssh_host_ed25519_key";
374 comment = "key comment";
375 }
376 ];
377 description = ''
378 NixOS can automatically generate SSH host keys. This option
379 specifies the path, type and size of each key. See
380 {manpage}`ssh-keygen(1)` for supported types
381 and sizes.
382 '';
383 };
384
385 banner = lib.mkOption {
386 type = lib.types.nullOr lib.types.lines;
387 default = null;
388 description = ''
389 Message to display to the remote user before authentication is allowed.
390 '';
391 };
392
393 authorizedKeysFiles = lib.mkOption {
394 type = lib.types.listOf lib.types.str;
395 default = [ ];
396 description = ''
397 Specify the rules for which files to read on the host.
398
399 This is an advanced option. If you're looking to configure user
400 keys, you can generally use [](#opt-users.users._name_.openssh.authorizedKeys.keys)
401 or [](#opt-users.users._name_.openssh.authorizedKeys.keyFiles).
402
403 These are paths relative to the host root file system or home
404 directories and they are subject to certain token expansion rules.
405 See AuthorizedKeysFile in man sshd_config for details.
406 '';
407 };
408
409 authorizedKeysInHomedir = lib.mkOption {
410 type = lib.types.bool;
411 default = true;
412 description = ''
413 Enables the use of the `~/.ssh/authorized_keys` file.
414
415 Otherwise, the only files trusted by default are those in `/etc/ssh/authorized_keys.d`,
416 *i.e.* SSH keys from [](#opt-users.users._name_.openssh.authorizedKeys.keys).
417 '';
418 };
419
420 authorizedKeysCommand = lib.mkOption {
421 type = lib.types.str;
422 default = "none";
423 description = ''
424 Specifies a program to be used to look up the user's public
425 keys. The program must be owned by root, not writable by group
426 or others and specified by an absolute path.
427 '';
428 };
429
430 authorizedKeysCommandUser = lib.mkOption {
431 type = lib.types.str;
432 default = "nobody";
433 description = ''
434 Specifies the user under whose account the AuthorizedKeysCommand
435 is run. It is recommended to use a dedicated user that has no
436 other role on the host than running authorized keys commands.
437 '';
438 };
439
440 settings = lib.mkOption {
441 description = "Configuration for `sshd_config(5)`.";
442 default = { };
443 example = lib.literalExpression ''
444 {
445 UseDns = true;
446 PasswordAuthentication = false;
447 }
448 '';
449 type = lib.types.submodule (
450 { name, ... }:
451 {
452 freeformType = settingsFormat.type;
453 options = {
454 AuthorizedPrincipalsFile = lib.mkOption {
455 type = lib.types.nullOr lib.types.str;
456 default = "none"; # upstream default
457 description = ''
458 Specifies a file that lists principal names that are accepted for certificate authentication. The default
459 is `"none"`, i.e. not to use a principals file.
460 '';
461 };
462 LogLevel = lib.mkOption {
463 type = lib.types.nullOr (
464 lib.types.enum [
465 "QUIET"
466 "FATAL"
467 "ERROR"
468 "INFO"
469 "VERBOSE"
470 "DEBUG"
471 "DEBUG1"
472 "DEBUG2"
473 "DEBUG3"
474 ]
475 );
476 default = "INFO"; # upstream default
477 description = ''
478 Gives the verbosity level that is used when logging messages from {manpage}`sshd(8)`. Logging with a DEBUG level
479 violates the privacy of users and is not recommended.
480 '';
481 };
482 UsePAM = lib.mkEnableOption "PAM authentication" // {
483 default = true;
484 type = lib.types.nullOr lib.types.bool;
485 };
486 UseDns = lib.mkOption {
487 type = lib.types.nullOr lib.types.bool;
488 # apply if cfg.useDns then "yes" else "no"
489 default = false;
490 description = ''
491 Specifies whether {manpage}`sshd(8)` should look up the remote host name, and to check that the resolved host name for
492 the remote IP address maps back to the very same IP address.
493 If this option is set to no (the default) then only addresses and not host names may be used in
494 ~/.ssh/authorized_keys from and sshd_config Match Host directives.
495 '';
496 };
497 X11Forwarding = lib.mkOption {
498 type = lib.types.nullOr lib.types.bool;
499 default = false;
500 description = ''
501 Whether to allow X11 connections to be forwarded.
502 '';
503 };
504 PasswordAuthentication = lib.mkOption {
505 type = lib.types.nullOr lib.types.bool;
506 default = true;
507 description = ''
508 Specifies whether password authentication is allowed.
509 '';
510 };
511 PermitRootLogin = lib.mkOption {
512 default = "prohibit-password";
513 type = lib.types.nullOr (
514 lib.types.enum [
515 "yes"
516 "without-password"
517 "prohibit-password"
518 "forced-commands-only"
519 "no"
520 ]
521 );
522 description = ''
523 Whether the root user can login using ssh.
524 '';
525 };
526 KbdInteractiveAuthentication = lib.mkOption {
527 type = lib.types.nullOr lib.types.bool;
528 default = true;
529 description = ''
530 Specifies whether keyboard-interactive authentication is allowed.
531 '';
532 };
533 GatewayPorts = lib.mkOption {
534 type = lib.types.nullOr lib.types.str;
535 default = "no";
536 description = ''
537 Specifies whether remote hosts are allowed to connect to
538 ports forwarded for the client. See
539 {manpage}`sshd_config(5)`.
540 '';
541 };
542 KexAlgorithms = lib.mkOption {
543 type = lib.types.nullOr (lib.types.listOf lib.types.str);
544 default = [
545 "mlkem768x25519-sha256"
546 "sntrup761x25519-sha512"
547 "sntrup761x25519-sha512@openssh.com"
548 "curve25519-sha256"
549 "curve25519-sha256@libssh.org"
550 "diffie-hellman-group-exchange-sha256"
551 ];
552 description = ''
553 Allowed key exchange algorithms
554
555 Uses the lower bound recommended in both
556 <https://stribika.github.io/2015/01/04/secure-secure-shell.html>
557 and
558 <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67>
559 '';
560 };
561 Macs = lib.mkOption {
562 type = lib.types.nullOr (lib.types.listOf lib.types.str);
563 default = [
564 "hmac-sha2-512-etm@openssh.com"
565 "hmac-sha2-256-etm@openssh.com"
566 "umac-128-etm@openssh.com"
567 ];
568 description = ''
569 Allowed MACs
570
571 Defaults to recommended settings from both
572 <https://stribika.github.io/2015/01/04/secure-secure-shell.html>
573 and
574 <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67>
575 '';
576 };
577 StrictModes = lib.mkOption {
578 type = lib.types.nullOr (lib.types.bool);
579 default = true;
580 description = ''
581 Whether sshd should check file modes and ownership of directories
582 '';
583 };
584 Ciphers = lib.mkOption {
585 type = lib.types.nullOr (lib.types.listOf lib.types.str);
586 default = [
587 "chacha20-poly1305@openssh.com"
588 "aes256-gcm@openssh.com"
589 "aes128-gcm@openssh.com"
590 "aes256-ctr"
591 "aes192-ctr"
592 "aes128-ctr"
593 ];
594 description = ''
595 Allowed ciphers
596
597 Defaults to recommended settings from both
598 <https://stribika.github.io/2015/01/04/secure-secure-shell.html>
599 and
600 <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67>
601 '';
602 };
603 AllowUsers = lib.mkOption {
604 type = with lib.types; nullOr (listOf str);
605 default = null;
606 description = ''
607 If specified, login is allowed only for the listed users.
608 See {manpage}`sshd_config(5)` for details.
609 '';
610 };
611 DenyUsers = lib.mkOption {
612 type = with lib.types; nullOr (listOf str);
613 default = null;
614 description = ''
615 If specified, login is denied for all listed users. Takes
616 precedence over [](#opt-services.openssh.settings.AllowUsers).
617 See {manpage}`sshd_config(5)` for details.
618 '';
619 };
620 AllowGroups = lib.mkOption {
621 type = with lib.types; nullOr (listOf str);
622 default = null;
623 description = ''
624 If specified, login is allowed only for users part of the
625 listed groups.
626 See {manpage}`sshd_config(5)` for details.
627 '';
628 };
629 DenyGroups = lib.mkOption {
630 type = with lib.types; nullOr (listOf str);
631 default = null;
632 description = ''
633 If specified, login is denied for all users part of the listed
634 groups. Takes precedence over
635 [](#opt-services.openssh.settings.AllowGroups). See
636 {manpage}`sshd_config(5)` for details.
637 '';
638 };
639 # Disabled by default, since pam_motd handles this.
640 PrintMotd = lib.mkEnableOption "printing /etc/motd when a user logs in interactively" // {
641 type = lib.types.nullOr lib.types.bool;
642 };
643 };
644 }
645 );
646 };
647
648 extraConfig = lib.mkOption {
649 type = lib.types.lines;
650 default = "";
651 description = "Verbatim contents of {file}`sshd_config`.";
652 };
653
654 moduliFile = lib.mkOption {
655 example = "/etc/my-local-ssh-moduli;";
656 type = lib.types.path;
657 description = ''
658 Path to `moduli` file to install in
659 `/etc/ssh/moduli`. If this option is unset, then
660 the `moduli` file shipped with OpenSSH will be used.
661 '';
662 };
663
664 };
665
666 users.users = lib.mkOption {
667 type = with lib.types; attrsOf (submodule userOptions);
668 };
669
670 };
671
672 ###### implementation
673
674 config = lib.mkIf cfg.enable {
675
676 users.users.sshd = {
677 isSystemUser = true;
678 group = "sshd";
679 description = "SSH privilege separation user";
680 };
681 users.groups.sshd = { };
682
683 services.openssh.moduliFile = lib.mkDefault "${cfg.package}/etc/ssh/moduli";
684 services.openssh.sftpServerExecutable = lib.mkDefault "${cfg.package}/libexec/sftp-server";
685
686 environment.etc =
687 authKeysFiles
688 // authPrincipalsFiles
689 // {
690 "ssh/moduli".source = cfg.moduliFile;
691 "ssh/sshd_config".source = sshconf;
692 };
693
694 systemd.tmpfiles.settings."ssh-root-provision" = {
695 "/root"."d-" = {
696 user = "root";
697 group = ":root";
698 mode = ":700";
699 };
700 "/root/.ssh"."d-" = {
701 user = "root";
702 group = ":root";
703 mode = ":700";
704 };
705 "/root/.ssh/authorized_keys"."f^" = {
706 user = "root";
707 group = ":root";
708 mode = ":600";
709 argument = "ssh.authorized_keys.root";
710 };
711 };
712
713 systemd = {
714 sockets.sshd = lib.mkIf cfg.startWhenNeeded {
715 description = "SSH Socket";
716 wantedBy = [ "sockets.target" ];
717 socketConfig.ListenStream =
718 if cfg.listenAddresses != [ ] then
719 lib.concatMap (
720 { addr, port }:
721 if port != null then [ "${addr}:${toString port}" ] else map (p: "${addr}:${toString p}") cfg.ports
722 ) cfg.listenAddresses
723 else
724 cfg.ports;
725 socketConfig.Accept = true;
726 # Prevent brute-force attacks from shutting down socket
727 socketConfig.TriggerLimitIntervalSec = 0;
728 };
729
730 services."sshd@" = {
731 description = "SSH per-connection Daemon";
732 after = [
733 "network.target"
734 "sshd-keygen.service"
735 ];
736 wants = [ "sshd-keygen.service" ];
737 stopIfChanged = false;
738 path = [ cfg.package ];
739 environment.LD_LIBRARY_PATH = nssModulesPath;
740
741 serviceConfig = {
742 ExecStart = lib.concatStringsSep " " [
743 "-${lib.getExe' cfg.package "sshd"}"
744 "-i"
745 "-D"
746 "-f /etc/ssh/sshd_config"
747 ];
748 KillMode = "process";
749 StandardInput = "socket";
750 StandardError = "journal";
751 };
752 };
753
754 services.sshd = lib.mkIf (!cfg.startWhenNeeded) {
755 description = "SSH Daemon";
756 wantedBy = [ "multi-user.target" ];
757 after = [
758 "network.target"
759 "sshd-keygen.service"
760 ];
761 wants = [ "sshd-keygen.service" ];
762 stopIfChanged = false;
763 path = [ cfg.package ];
764 environment.LD_LIBRARY_PATH = nssModulesPath;
765
766 restartTriggers = [ config.environment.etc."ssh/sshd_config".source ];
767
768 serviceConfig = {
769 Type = "notify-reload";
770 Restart = "always";
771 ExecStart = lib.concatStringsSep " " [
772 (lib.getExe' cfg.package "sshd")
773 "-D"
774 "-f"
775 "/etc/ssh/sshd_config"
776 ];
777 KillMode = "process";
778 };
779 };
780
781 services.sshd-keygen = {
782 description = "SSH Host Keys Generation";
783 unitConfig = {
784 ConditionFileNotEmpty = map (k: "|!${k.path}") cfg.hostKeys;
785 };
786 serviceConfig = {
787 Type = "oneshot";
788 };
789 path = [ cfg.package ];
790 script = lib.flip lib.concatMapStrings cfg.hostKeys (k: ''
791 if ! [ -s "${k.path}" ]; then
792 if ! [ -h "${k.path}" ]; then
793 rm -f "${k.path}"
794 fi
795 mkdir -p "$(dirname '${k.path}')"
796 chmod 0755 "$(dirname '${k.path}')"
797 ssh-keygen \
798 -t "${k.type}" \
799 ${lib.optionalString (k ? bits) "-b ${toString k.bits}"} \
800 ${lib.optionalString (k ? comment) "-C '${k.comment}'"} \
801 ${lib.optionalString (k ? openSSHFormat && k.openSSHFormat) "-o"} \
802 -f "${k.path}" \
803 -N ""
804 fi
805 '');
806 };
807 };
808
809 networking.firewall.allowedTCPPorts = lib.optionals cfg.openFirewall cfg.ports;
810
811 security.pam.services.sshd = lib.mkIf cfg.settings.UsePAM {
812 startSession = true;
813 showMotd = true;
814 unixAuth = if cfg.settings.PasswordAuthentication == true then true else false;
815 };
816
817 # These values are merged with the ones defined externally, see:
818 # https://github.com/NixOS/nixpkgs/pull/10155
819 # https://github.com/NixOS/nixpkgs/pull/41745
820 services.openssh.authorizedKeysFiles =
821 lib.optional cfg.authorizedKeysInHomedir "%h/.ssh/authorized_keys"
822 ++ [ "/etc/ssh/authorized_keys.d/%u" ];
823
824 services.openssh.settings.AuthorizedPrincipalsFile = lib.mkIf (
825 authPrincipalsFiles != { }
826 ) "/etc/ssh/authorized_principals.d/%u";
827
828 services.openssh.extraConfig = lib.mkOrder 0 ''
829 Banner ${if cfg.banner == null then "none" else pkgs.writeText "ssh_banner" cfg.banner}
830
831 AddressFamily ${if config.networking.enableIPv6 then "any" else "inet"}
832 ${lib.concatMapStrings (port: ''
833 Port ${toString port}
834 '') cfg.ports}
835
836 ${lib.concatMapStrings (
837 { port, addr, ... }:
838 ''
839 ListenAddress ${addr}${lib.optionalString (port != null) (":" + toString port)}
840 ''
841 ) cfg.listenAddresses}
842
843 ${lib.optionalString cfgc.setXAuthLocation ''
844 XAuthLocation ${pkgs.xorg.xauth}/bin/xauth
845 ''}
846 ${lib.optionalString cfg.allowSFTP ''
847 Subsystem sftp ${cfg.sftpServerExecutable} ${lib.concatStringsSep " " cfg.sftpFlags}
848 ''}
849 AuthorizedKeysFile ${toString cfg.authorizedKeysFiles}
850 ${lib.optionalString (cfg.authorizedKeysCommand != "none") ''
851 AuthorizedKeysCommand ${cfg.authorizedKeysCommand}
852 AuthorizedKeysCommandUser ${cfg.authorizedKeysCommandUser}
853 ''}
854
855 ${lib.flip lib.concatMapStrings cfg.hostKeys (k: ''
856 HostKey ${k.path}
857 '')}
858 '';
859
860 system.checks = [
861 (pkgs.runCommand "check-sshd-config"
862 {
863 nativeBuildInputs = [ validationPackage ];
864 }
865 ''
866 ${lib.concatMapStringsSep "\n" (
867 lport: "sshd -G -T -C lport=${toString lport} -f ${sshconf} > /dev/null"
868 ) cfg.ports}
869 ${lib.concatMapStringsSep "\n" (
870 la:
871 lib.concatMapStringsSep "\n" (
872 port:
873 "sshd -G -T -C ${lib.escapeShellArg "laddr=${la.addr},lport=${toString port}"} -f ${sshconf} > /dev/null"
874 ) (if la.port != null then [ la.port ] else cfg.ports)
875 ) cfg.listenAddresses}
876 touch $out
877 ''
878 )
879 ];
880
881 assertions = [
882 {
883 assertion = if cfg.settings.X11Forwarding then cfgc.setXAuthLocation else true;
884 message = "cannot enable X11 forwarding without setting xauth location";
885 }
886 {
887 assertion =
888 (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}")
889 != null
890 -> cfgc.package.withKerberos;
891 message = "cannot enable Kerberos authentication without using a package with Kerberos support";
892 }
893 {
894 assertion =
895 (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}")
896 != null
897 -> cfgc.package.withKerberos;
898 message = "cannot enable GSSAPI authentication without using a package with Kerberos support";
899 }
900 (
901 let
902 duplicates =
903 # Filter out the groups with more than 1 element
904 lib.filter (l: lib.length l > 1) (
905 # Grab the groups, we don't care about the group identifiers
906 lib.attrValues (
907 # Group the settings that are the same in lower case
908 lib.groupBy lib.strings.toLower (lib.attrNames cfg.settings)
909 )
910 );
911 formattedDuplicates = lib.concatMapStringsSep ", " (
912 dupl: "(${lib.concatStringsSep ", " dupl})"
913 ) duplicates;
914 in
915 {
916 assertion = lib.length duplicates == 0;
917 message = ''Duplicate sshd config key; does your capitalization match the option's? Duplicate keys: ${formattedDuplicates}'';
918 }
919 )
920 ]
921 ++ lib.forEach cfg.listenAddresses (
922 { addr, ... }:
923 {
924 assertion = addr != null;
925 message = "addr must be specified in each listenAddresses entry";
926 }
927 );
928 };
929
930}