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