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.extraUsers (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 description = ''
202 NixOS can automatically generate SSH host keys. This option
203 specifies the path, type and size of each key. See
204 <citerefentry><refentrytitle>ssh-keygen</refentrytitle>
205 <manvolnum>1</manvolnum></citerefentry> for supported types
206 and sizes.
207 '';
208 };
209
210 authorizedKeysFiles = mkOption {
211 type = types.listOf types.str;
212 default = [];
213 description = "Files from which authorized keys are read.";
214 };
215
216 extraConfig = mkOption {
217 type = types.lines;
218 default = "";
219 description = "Verbatim contents of <filename>sshd_config</filename>.";
220 };
221
222 moduliFile = mkOption {
223 example = "/etc/my-local-ssh-moduli;";
224 type = types.path;
225 description = ''
226 Path to <literal>moduli</literal> file to install in
227 <literal>/etc/ssh/moduli</literal>. If this option is unset, then
228 the <literal>moduli</literal> file shipped with OpenSSH will be used.
229 '';
230 };
231
232 };
233
234 users.users = mkOption {
235 options = [ userOptions ];
236 };
237
238 };
239
240
241 ###### implementation
242
243 config = mkIf cfg.enable {
244
245 users.extraUsers.sshd =
246 { isSystemUser = true;
247 description = "SSH privilege separation user";
248 };
249
250 services.openssh.moduliFile = mkDefault "${cfgc.package}/etc/ssh/moduli";
251
252 environment.etc = authKeysFiles //
253 { "ssh/moduli".source = cfg.moduliFile; };
254
255 systemd =
256 let
257 service =
258 { description = "SSH Daemon";
259 wantedBy = optional (!cfg.startWhenNeeded) "multi-user.target";
260 after = [ "network.target" ];
261 stopIfChanged = false;
262 path = [ cfgc.package pkgs.gawk ];
263 environment.LD_LIBRARY_PATH = nssModulesPath;
264
265 preStart =
266 ''
267 # Make sure we don't write to stdout, since in case of
268 # socket activation, it goes to the remote side (#19589).
269 exec >&2
270
271 mkdir -m 0755 -p /etc/ssh
272
273 ${flip concatMapStrings cfg.hostKeys (k: ''
274 if ! [ -f "${k.path}" ]; then
275 ssh-keygen -t "${k.type}" ${if k ? bits then "-b ${toString k.bits}" else ""} -f "${k.path}" -N ""
276 fi
277 '')}
278 '';
279
280 serviceConfig =
281 { ExecStart =
282 (optionalString cfg.startWhenNeeded "-") +
283 "${cfgc.package}/bin/sshd " + (optionalString cfg.startWhenNeeded "-i ") +
284 "-f ${pkgs.writeText "sshd_config" cfg.extraConfig}";
285 KillMode = "process";
286 } // (if cfg.startWhenNeeded then {
287 StandardInput = "socket";
288 StandardError = "journal";
289 } else {
290 Restart = "always";
291 Type = "simple";
292 });
293 };
294 in
295
296 if cfg.startWhenNeeded then {
297
298 sockets.sshd =
299 { description = "SSH Socket";
300 wantedBy = [ "sockets.target" ];
301 socketConfig.ListenStream = cfg.ports;
302 socketConfig.Accept = true;
303 };
304
305 services."sshd@" = service;
306
307 } else {
308
309 services.sshd = service;
310
311 };
312
313 networking.firewall.allowedTCPPorts = if cfg.openFirewall then cfg.ports else [];
314
315 security.pam.services.sshd =
316 { startSession = true;
317 showMotd = true;
318 unixAuth = cfg.passwordAuthentication;
319 };
320
321 services.openssh.authorizedKeysFiles =
322 [ ".ssh/authorized_keys" ".ssh/authorized_keys2" "/etc/ssh/authorized_keys.d/%u" ];
323
324 services.openssh.extraConfig = mkOrder 0
325 ''
326 Protocol 2
327
328 UsePAM yes
329
330 AddressFamily ${if config.networking.enableIPv6 then "any" else "inet"}
331 ${concatMapStrings (port: ''
332 Port ${toString port}
333 '') cfg.ports}
334
335 ${concatMapStrings ({ port, addr, ... }: ''
336 ListenAddress ${addr}${if port != null then ":" + toString port else ""}
337 '') cfg.listenAddresses}
338
339 ${optionalString cfgc.setXAuthLocation ''
340 XAuthLocation ${pkgs.xorg.xauth}/bin/xauth
341 ''}
342
343 ${if cfg.forwardX11 then ''
344 X11Forwarding yes
345 '' else ''
346 X11Forwarding no
347 ''}
348
349 ${optionalString cfg.allowSFTP ''
350 Subsystem sftp ${cfgc.package}/libexec/sftp-server ${concatStringsSep " " cfg.sftpFlags}
351 ''}
352
353 PermitRootLogin ${cfg.permitRootLogin}
354 GatewayPorts ${cfg.gatewayPorts}
355 PasswordAuthentication ${if cfg.passwordAuthentication then "yes" else "no"}
356 ChallengeResponseAuthentication ${if cfg.challengeResponseAuthentication then "yes" else "no"}
357
358 PrintMotd no # handled by pam_motd
359
360 AuthorizedKeysFile ${toString cfg.authorizedKeysFiles}
361
362 ${flip concatMapStrings cfg.hostKeys (k: ''
363 HostKey ${k.path}
364 '')}
365
366 ### Recommended settings from both:
367 # https://stribika.github.io/2015/01/04/secure-secure-shell.html
368 # and
369 # https://wiki.mozilla.org/Security/Guidelines/OpenSSH#Modern_.28OpenSSH_6.7.2B.29
370
371 KexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256
372 Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr
373 MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-512,hmac-sha2-256,umac-128@openssh.com
374
375 # LogLevel VERBOSE logs user's key fingerprint on login.
376 # Needed to have a clear audit track of which key was used to log in.
377 LogLevel VERBOSE
378 '';
379
380 assertions = [{ assertion = if cfg.forwardX11 then cfgc.setXAuthLocation else true;
381 message = "cannot enable X11 forwarding without setting xauth location";}]
382 ++ flip map cfg.listenAddresses ({ addr, port, ... }: {
383 assertion = addr != null;
384 message = "addr must be specified in each listenAddresses entry";
385 });
386
387 };
388
389}