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 sshconf = pkgs.runCommand "sshd.conf-validated" { nativeBuildInputs = [ validationPackage ]; } ''
16 cat >$out <<EOL
17 ${cfg.extraConfig}
18 EOL
19
20 ssh-keygen -q -f mock-hostkey -N ""
21 sshd -t -f $out -h mock-hostkey
22 '';
23
24 cfg = config.services.openssh;
25 cfgc = config.programs.ssh;
26
27 nssModulesPath = config.system.nssModules.path;
28
29 userOptions = {
30
31 options.openssh.authorizedKeys = {
32 keys = mkOption {
33 type = types.listOf types.singleLineStr;
34 default = [];
35 description = lib.mdDoc ''
36 A list of verbatim OpenSSH public keys that should be added to the
37 user's authorized keys. The keys are added to a file that the SSH
38 daemon reads in addition to the the user's authorized_keys file.
39 You can combine the `keys` and
40 `keyFiles` options.
41 Warning: If you are using `NixOps` then don't use this
42 option since it will replace the key required for deployment via ssh.
43 '';
44 example = [
45 "ssh-rsa AAAAB3NzaC1yc2etc/etc/etcjwrsh8e596z6J0l7 example@host"
46 "ssh-ed25519 AAAAC3NzaCetcetera/etceteraJZMfk3QPfQ foo@bar"
47 ];
48 };
49
50 keyFiles = mkOption {
51 type = types.listOf types.path;
52 default = [];
53 description = lib.mdDoc ''
54 A list of files each containing one OpenSSH public key that should be
55 added to the user's authorized keys. The contents of the files are
56 read at build time and added to a file that the SSH daemon reads in
57 addition to the the user's authorized_keys file. You can combine the
58 `keyFiles` and `keys` options.
59 '';
60 };
61 };
62
63 };
64
65 authKeysFiles = let
66 mkAuthKeyFile = u: nameValuePair "ssh/authorized_keys.d/${u.name}" {
67 mode = "0444";
68 source = pkgs.writeText "${u.name}-authorized_keys" ''
69 ${concatStringsSep "\n" u.openssh.authorizedKeys.keys}
70 ${concatMapStrings (f: readFile f + "\n") u.openssh.authorizedKeys.keyFiles}
71 '';
72 };
73 usersWithKeys = attrValues (flip filterAttrs config.users.users (n: u:
74 length u.openssh.authorizedKeys.keys != 0 || length u.openssh.authorizedKeys.keyFiles != 0
75 ));
76 in listToAttrs (map mkAuthKeyFile usersWithKeys);
77
78in
79
80{
81 imports = [
82 (mkAliasOptionModule [ "services" "sshd" "enable" ] [ "services" "openssh" "enable" ])
83 (mkAliasOptionModule [ "services" "openssh" "knownHosts" ] [ "programs" "ssh" "knownHosts" ])
84 (mkRenamedOptionModule [ "services" "openssh" "challengeResponseAuthentication" ] [ "services" "openssh" "kbdInteractiveAuthentication" ])
85 ];
86
87 ###### interface
88
89 options = {
90
91 services.openssh = {
92
93 enable = mkOption {
94 type = types.bool;
95 default = false;
96 description = lib.mdDoc ''
97 Whether to enable the OpenSSH secure shell daemon, which
98 allows secure remote logins.
99 '';
100 };
101
102 startWhenNeeded = mkOption {
103 type = types.bool;
104 default = false;
105 description = lib.mdDoc ''
106 If set, {command}`sshd` is socket-activated; that
107 is, instead of having it permanently running as a daemon,
108 systemd will start an instance for each incoming connection.
109 '';
110 };
111
112 forwardX11 = mkOption {
113 type = types.bool;
114 default = false;
115 description = lib.mdDoc ''
116 Whether to allow X11 connections to be forwarded.
117 '';
118 };
119
120 allowSFTP = mkOption {
121 type = types.bool;
122 default = true;
123 description = lib.mdDoc ''
124 Whether to enable the SFTP subsystem in the SSH daemon. This
125 enables the use of commands such as {command}`sftp` and
126 {command}`sshfs`.
127 '';
128 };
129
130 sftpServerExecutable = mkOption {
131 type = types.str;
132 example = "internal-sftp";
133 description = lib.mdDoc ''
134 The sftp server executable. Can be a path or "internal-sftp" to use
135 the sftp server built into the sshd binary.
136 '';
137 };
138
139 sftpFlags = mkOption {
140 type = with types; listOf str;
141 default = [];
142 example = [ "-f AUTHPRIV" "-l INFO" ];
143 description = lib.mdDoc ''
144 Commandline flags to add to sftp-server.
145 '';
146 };
147
148 permitRootLogin = mkOption {
149 default = "prohibit-password";
150 type = types.enum ["yes" "without-password" "prohibit-password" "forced-commands-only" "no"];
151 description = lib.mdDoc ''
152 Whether the root user can login using ssh.
153 '';
154 };
155
156 gatewayPorts = mkOption {
157 type = types.str;
158 default = "no";
159 description = lib.mdDoc ''
160 Specifies whether remote hosts are allowed to connect to
161 ports forwarded for the client. See
162 {manpage}`sshd_config(5)`.
163 '';
164 };
165
166 ports = mkOption {
167 type = types.listOf types.port;
168 default = [22];
169 description = lib.mdDoc ''
170 Specifies on which ports the SSH daemon listens.
171 '';
172 };
173
174 openFirewall = mkOption {
175 type = types.bool;
176 default = true;
177 description = lib.mdDoc ''
178 Whether to automatically open the specified ports in the firewall.
179 '';
180 };
181
182 listenAddresses = mkOption {
183 type = with types; listOf (submodule {
184 options = {
185 addr = mkOption {
186 type = types.nullOr types.str;
187 default = null;
188 description = lib.mdDoc ''
189 Host, IPv4 or IPv6 address to listen to.
190 '';
191 };
192 port = mkOption {
193 type = types.nullOr types.int;
194 default = null;
195 description = lib.mdDoc ''
196 Port to listen to.
197 '';
198 };
199 };
200 });
201 default = [];
202 example = [ { addr = "192.168.3.1"; port = 22; } { addr = "0.0.0.0"; port = 64022; } ];
203 description = lib.mdDoc ''
204 List of addresses and ports to listen on (ListenAddress directive
205 in config). If port is not specified for address sshd will listen
206 on all ports specified by `ports` option.
207 NOTE: this will override default listening on all local addresses and port 22.
208 NOTE: setting this option won't automatically enable given ports
209 in firewall configuration.
210 '';
211 };
212
213 passwordAuthentication = mkOption {
214 type = types.bool;
215 default = true;
216 description = lib.mdDoc ''
217 Specifies whether password authentication is allowed.
218 '';
219 };
220
221 kbdInteractiveAuthentication = mkOption {
222 type = types.bool;
223 default = true;
224 description = lib.mdDoc ''
225 Specifies whether keyboard-interactive authentication is allowed.
226 '';
227 };
228
229 hostKeys = mkOption {
230 type = types.listOf types.attrs;
231 default =
232 [ { type = "rsa"; bits = 4096; path = "/etc/ssh/ssh_host_rsa_key"; }
233 { type = "ed25519"; path = "/etc/ssh/ssh_host_ed25519_key"; }
234 ];
235 example =
236 [ { type = "rsa"; bits = 4096; path = "/etc/ssh/ssh_host_rsa_key"; rounds = 100; openSSHFormat = true; }
237 { type = "ed25519"; path = "/etc/ssh/ssh_host_ed25519_key"; rounds = 100; comment = "key comment"; }
238 ];
239 description = lib.mdDoc ''
240 NixOS can automatically generate SSH host keys. This option
241 specifies the path, type and size of each key. See
242 {manpage}`ssh-keygen(1)` for supported types
243 and sizes.
244 '';
245 };
246
247 banner = mkOption {
248 type = types.nullOr types.lines;
249 default = null;
250 description = lib.mdDoc ''
251 Message to display to the remote user before authentication is allowed.
252 '';
253 };
254
255 authorizedKeysFiles = mkOption {
256 type = types.listOf types.str;
257 default = [];
258 description = lib.mdDoc ''
259 Specify the rules for which files to read on the host.
260
261 This is an advanced option. If you're looking to configure user
262 keys, you can generally use [](#opt-users.users._name_.openssh.authorizedKeys.keys)
263 or [](#opt-users.users._name_.openssh.authorizedKeys.keyFiles).
264
265 These are paths relative to the host root file system or home
266 directories and they are subject to certain token expansion rules.
267 See AuthorizedKeysFile in man sshd_config for details.
268 '';
269 };
270
271 authorizedKeysCommand = mkOption {
272 type = types.str;
273 default = "none";
274 description = lib.mdDoc ''
275 Specifies a program to be used to look up the user's public
276 keys. The program must be owned by root, not writable by group
277 or others and specified by an absolute path.
278 '';
279 };
280
281 authorizedKeysCommandUser = mkOption {
282 type = types.str;
283 default = "nobody";
284 description = lib.mdDoc ''
285 Specifies the user under whose account the AuthorizedKeysCommand
286 is run. It is recommended to use a dedicated user that has no
287 other role on the host than running authorized keys commands.
288 '';
289 };
290
291 kexAlgorithms = mkOption {
292 type = types.listOf types.str;
293 default = [
294 "sntrup761x25519-sha512@openssh.com"
295 "curve25519-sha256"
296 "curve25519-sha256@libssh.org"
297 "diffie-hellman-group-exchange-sha256"
298 ];
299 description = lib.mdDoc ''
300 Allowed key exchange algorithms
301
302 Uses the lower bound recommended in both
303 <https://stribika.github.io/2015/01/04/secure-secure-shell.html>
304 and
305 <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67>
306 '';
307 };
308
309 ciphers = mkOption {
310 type = types.listOf types.str;
311 default = [
312 "chacha20-poly1305@openssh.com"
313 "aes256-gcm@openssh.com"
314 "aes128-gcm@openssh.com"
315 "aes256-ctr"
316 "aes192-ctr"
317 "aes128-ctr"
318 ];
319 description = lib.mdDoc ''
320 Allowed ciphers
321
322 Defaults to recommended settings from both
323 <https://stribika.github.io/2015/01/04/secure-secure-shell.html>
324 and
325 <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67>
326 '';
327 };
328
329 macs = mkOption {
330 type = types.listOf types.str;
331 default = [
332 "hmac-sha2-512-etm@openssh.com"
333 "hmac-sha2-256-etm@openssh.com"
334 "umac-128-etm@openssh.com"
335 "hmac-sha2-512"
336 "hmac-sha2-256"
337 "umac-128@openssh.com"
338 ];
339 description = lib.mdDoc ''
340 Allowed MACs
341
342 Defaults to recommended settings from both
343 <https://stribika.github.io/2015/01/04/secure-secure-shell.html>
344 and
345 <https://infosec.mozilla.org/guidelines/openssh#modern-openssh-67>
346 '';
347 };
348
349 logLevel = mkOption {
350 type = types.enum [ "QUIET" "FATAL" "ERROR" "INFO" "VERBOSE" "DEBUG" "DEBUG1" "DEBUG2" "DEBUG3" ];
351 default = "INFO"; # upstream default
352 description = lib.mdDoc ''
353 Gives the verbosity level that is used when logging messages from sshd(8). The possible values are:
354 QUIET, FATAL, ERROR, INFO, VERBOSE, DEBUG, DEBUG1, DEBUG2, and DEBUG3. The default is INFO. DEBUG and DEBUG1
355 are equivalent. DEBUG2 and DEBUG3 each specify higher levels of debugging output. Logging with a DEBUG level
356 violates the privacy of users and is not recommended.
357 '';
358 };
359
360 useDns = mkOption {
361 type = types.bool;
362 default = false;
363 description = lib.mdDoc ''
364 Specifies whether sshd(8) should look up the remote host name, and to check that the resolved host name for
365 the remote IP address maps back to the very same IP address.
366 If this option is set to no (the default) then only addresses and not host names may be used in
367 ~/.ssh/authorized_keys from and sshd_config Match Host directives.
368 '';
369 };
370
371 extraConfig = mkOption {
372 type = types.lines;
373 default = "";
374 description = lib.mdDoc "Verbatim contents of {file}`sshd_config`.";
375 };
376
377 moduliFile = mkOption {
378 example = "/etc/my-local-ssh-moduli;";
379 type = types.path;
380 description = lib.mdDoc ''
381 Path to `moduli` file to install in
382 `/etc/ssh/moduli`. If this option is unset, then
383 the `moduli` file shipped with OpenSSH will be used.
384 '';
385 };
386
387 };
388
389 users.users = mkOption {
390 type = with types; attrsOf (submodule userOptions);
391 };
392
393 };
394
395
396 ###### implementation
397
398 config = mkIf cfg.enable {
399
400 users.users.sshd =
401 {
402 isSystemUser = true;
403 group = "sshd";
404 description = "SSH privilege separation user";
405 };
406 users.groups.sshd = {};
407
408 services.openssh.moduliFile = mkDefault "${cfgc.package}/etc/ssh/moduli";
409 services.openssh.sftpServerExecutable = mkDefault "${cfgc.package}/libexec/sftp-server";
410
411 environment.etc = authKeysFiles //
412 { "ssh/moduli".source = cfg.moduliFile;
413 "ssh/sshd_config".source = sshconf;
414 };
415
416 systemd =
417 let
418 service =
419 { description = "SSH Daemon";
420 wantedBy = optional (!cfg.startWhenNeeded) "multi-user.target";
421 after = [ "network.target" ];
422 stopIfChanged = false;
423 path = [ cfgc.package pkgs.gawk ];
424 environment.LD_LIBRARY_PATH = nssModulesPath;
425
426 restartTriggers = optionals (!cfg.startWhenNeeded) [
427 config.environment.etc."ssh/sshd_config".source
428 ];
429
430 preStart =
431 ''
432 # Make sure we don't write to stdout, since in case of
433 # socket activation, it goes to the remote side (#19589).
434 exec >&2
435
436 ${flip concatMapStrings cfg.hostKeys (k: ''
437 if ! [ -s "${k.path}" ]; then
438 if ! [ -h "${k.path}" ]; then
439 rm -f "${k.path}"
440 fi
441 mkdir -m 0755 -p "$(dirname '${k.path}')"
442 ssh-keygen \
443 -t "${k.type}" \
444 ${if k ? bits then "-b ${toString k.bits}" else ""} \
445 ${if k ? rounds then "-a ${toString k.rounds}" else ""} \
446 ${if k ? comment then "-C '${k.comment}'" else ""} \
447 ${if k ? openSSHFormat && k.openSSHFormat then "-o" else ""} \
448 -f "${k.path}" \
449 -N ""
450 fi
451 '')}
452 '';
453
454 serviceConfig =
455 { ExecStart =
456 (optionalString cfg.startWhenNeeded "-") +
457 "${cfgc.package}/bin/sshd " + (optionalString cfg.startWhenNeeded "-i ") +
458 "-D " + # don't detach into a daemon process
459 "-f /etc/ssh/sshd_config";
460 KillMode = "process";
461 } // (if cfg.startWhenNeeded then {
462 StandardInput = "socket";
463 StandardError = "journal";
464 } else {
465 Restart = "always";
466 Type = "simple";
467 });
468
469 };
470 in
471
472 if cfg.startWhenNeeded then {
473
474 sockets.sshd =
475 { description = "SSH Socket";
476 wantedBy = [ "sockets.target" ];
477 socketConfig.ListenStream = if cfg.listenAddresses != [] then
478 map (l: "${l.addr}:${toString (if l.port != null then l.port else 22)}") cfg.listenAddresses
479 else
480 cfg.ports;
481 socketConfig.Accept = true;
482 # Prevent brute-force attacks from shutting down socket
483 socketConfig.TriggerLimitIntervalSec = 0;
484 };
485
486 services."sshd@" = service;
487
488 } else {
489
490 services.sshd = service;
491
492 };
493
494 networking.firewall.allowedTCPPorts = if cfg.openFirewall then cfg.ports else [];
495
496 security.pam.services.sshd =
497 { startSession = true;
498 showMotd = true;
499 unixAuth = cfg.passwordAuthentication;
500 };
501
502 # These values are merged with the ones defined externally, see:
503 # https://github.com/NixOS/nixpkgs/pull/10155
504 # https://github.com/NixOS/nixpkgs/pull/41745
505 services.openssh.authorizedKeysFiles =
506 [ "%h/.ssh/authorized_keys" "%h/.ssh/authorized_keys2" "/etc/ssh/authorized_keys.d/%u" ];
507
508 services.openssh.extraConfig = mkOrder 0
509 ''
510 UsePAM yes
511
512 Banner ${if cfg.banner == null then "none" else pkgs.writeText "ssh_banner" cfg.banner}
513
514 AddressFamily ${if config.networking.enableIPv6 then "any" else "inet"}
515 ${concatMapStrings (port: ''
516 Port ${toString port}
517 '') cfg.ports}
518
519 ${concatMapStrings ({ port, addr, ... }: ''
520 ListenAddress ${addr}${if port != null then ":" + toString port else ""}
521 '') cfg.listenAddresses}
522
523 ${optionalString cfgc.setXAuthLocation ''
524 XAuthLocation ${pkgs.xorg.xauth}/bin/xauth
525 ''}
526
527 X11Forwarding ${if cfg.forwardX11 then "yes" else "no"}
528
529 ${optionalString cfg.allowSFTP ''
530 Subsystem sftp ${cfg.sftpServerExecutable} ${concatStringsSep " " cfg.sftpFlags}
531 ''}
532
533 PermitRootLogin ${cfg.permitRootLogin}
534 GatewayPorts ${cfg.gatewayPorts}
535 PasswordAuthentication ${if cfg.passwordAuthentication then "yes" else "no"}
536 KbdInteractiveAuthentication ${if cfg.kbdInteractiveAuthentication then "yes" else "no"}
537
538 PrintMotd no # handled by pam_motd
539
540 AuthorizedKeysFile ${toString cfg.authorizedKeysFiles}
541 ${optionalString (cfg.authorizedKeysCommand != "none") ''
542 AuthorizedKeysCommand ${cfg.authorizedKeysCommand}
543 AuthorizedKeysCommandUser ${cfg.authorizedKeysCommandUser}
544 ''}
545
546 ${flip concatMapStrings cfg.hostKeys (k: ''
547 HostKey ${k.path}
548 '')}
549
550 KexAlgorithms ${concatStringsSep "," cfg.kexAlgorithms}
551 Ciphers ${concatStringsSep "," cfg.ciphers}
552 MACs ${concatStringsSep "," cfg.macs}
553
554 LogLevel ${cfg.logLevel}
555
556 UseDNS ${if cfg.useDns then "yes" else "no"}
557
558 '';
559
560 assertions = [{ assertion = if cfg.forwardX11 then cfgc.setXAuthLocation else true;
561 message = "cannot enable X11 forwarding without setting xauth location";}]
562 ++ forEach cfg.listenAddresses ({ addr, ... }: {
563 assertion = addr != null;
564 message = "addr must be specified in each listenAddresses entry";
565 });
566
567 };
568
569}