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