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