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 knownHosts = map (h: getAttr h cfg.knownHosts) (attrNames cfg.knownHosts);
13
14 knownHostsText = flip (concatMapStringsSep "\n") knownHosts
15 (h:
16 concatStringsSep "," h.hostNames + " "
17 + (if h.publicKey != null then h.publicKey else readFile h.publicKeyFile)
18 );
19
20 userOptions = {
21
22 openssh.authorizedKeys = {
23 keys = mkOption {
24 type = types.listOf types.str;
25 default = [];
26 description = ''
27 A list of verbatim OpenSSH public keys that should be added to the
28 user's authorized keys. The keys are added to a file that the SSH
29 daemon reads in addition to the the user's authorized_keys file.
30 You can combine the <literal>keys</literal> and
31 <literal>keyFiles</literal> options.
32 '';
33 };
34
35 keyFiles = mkOption {
36 type = types.listOf types.path;
37 default = [];
38 description = ''
39 A list of files each containing one OpenSSH public key that should be
40 added to the user's authorized keys. The contents of the files are
41 read at build time and added to a file that the SSH daemon reads in
42 addition to the the user's authorized_keys file. You can combine the
43 <literal>keyFiles</literal> and <literal>keys</literal> options.
44 '';
45 };
46 };
47
48 };
49
50 authKeysFiles = let
51 mkAuthKeyFile = u: {
52 target = "ssh/authorized_keys.d/${u.name}";
53 mode = "0444";
54 source = pkgs.writeText "${u.name}-authorized_keys" ''
55 ${concatStringsSep "\n" u.openssh.authorizedKeys.keys}
56 ${concatMapStrings (f: readFile f + "\n") u.openssh.authorizedKeys.keyFiles}
57 '';
58 };
59 usersWithKeys = attrValues (flip filterAttrs config.users.extraUsers (n: u:
60 length u.openssh.authorizedKeys.keys != 0 || length u.openssh.authorizedKeys.keyFiles != 0
61 ));
62 in map mkAuthKeyFile usersWithKeys;
63
64in
65
66{
67
68 ###### interface
69
70 options = {
71
72 services.openssh = {
73
74 enable = mkOption {
75 type = types.bool;
76 default = false;
77 description = ''
78 Whether to enable the OpenSSH secure shell daemon, which
79 allows secure remote logins.
80 '';
81 };
82
83 startWhenNeeded = mkOption {
84 type = types.bool;
85 default = false;
86 description = ''
87 If set, <command>sshd</command> is socket-activated; that
88 is, instead of having it permanently running as a daemon,
89 systemd will start an instance for each incoming connection.
90 '';
91 };
92
93 forwardX11 = mkOption {
94 type = types.bool;
95 default = cfgc.setXAuthLocation;
96 description = ''
97 Whether to allow X11 connections to be forwarded.
98 '';
99 };
100
101 allowSFTP = mkOption {
102 type = types.bool;
103 default = true;
104 description = ''
105 Whether to enable the SFTP subsystem in the SSH daemon. This
106 enables the use of commands such as <command>sftp</command> and
107 <command>sshfs</command>.
108 '';
109 };
110
111 permitRootLogin = mkOption {
112 default = "without-password";
113 type = types.enum ["yes" "without-password" "forced-commands-only" "no"];
114 description = ''
115 Whether the root user can login using ssh.
116 '';
117 };
118
119 gatewayPorts = mkOption {
120 type = types.str;
121 default = "no";
122 description = ''
123 Specifies whether remote hosts are allowed to connect to
124 ports forwarded for the client. See
125 <citerefentry><refentrytitle>sshd_config</refentrytitle>
126 <manvolnum>5</manvolnum></citerefentry>.
127 '';
128 };
129
130 ports = mkOption {
131 type = types.listOf types.int;
132 default = [22];
133 description = ''
134 Specifies on which ports the SSH daemon listens.
135 '';
136 };
137
138 listenAddresses = mkOption {
139 type = types.listOf types.optionSet;
140 default = [];
141 example = [ { addr = "192.168.3.1"; port = 22; } { addr = "0.0.0.0"; port = 64022; } ];
142 description = ''
143 List of addresses and ports to listen on (ListenAddress directive
144 in config). If port is not specified for address sshd will listen
145 on all ports specified by <literal>ports</literal> option.
146 NOTE: this will override default listening on all local addresses and port 22.
147 NOTE: setting this option won't automatically enable given ports
148 in firewall configuration.
149 '';
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
168 passwordAuthentication = mkOption {
169 type = types.bool;
170 default = true;
171 description = ''
172 Specifies whether password authentication is allowed.
173 '';
174 };
175
176 challengeResponseAuthentication = mkOption {
177 type = types.bool;
178 default = true;
179 description = ''
180 Specifies whether challenge/response authentication is allowed.
181 '';
182 };
183
184 hostKeys = mkOption {
185 type = types.listOf types.attrs;
186 default =
187 [ { type = "rsa"; bits = 4096; path = "/etc/ssh/ssh_host_rsa_key"; }
188 { type = "ed25519"; path = "/etc/ssh/ssh_host_ed25519_key"; }
189 ] ++ optionals (!versionAtLeast config.system.stateVersion "15.07")
190 [ { type = "dsa"; path = "/etc/ssh/ssh_host_dsa_key"; }
191 { type = "ecdsa"; bits = 521; path = "/etc/ssh/ssh_host_ecdsa_key"; }
192 ];
193 description = ''
194 NixOS can automatically generate SSH host keys. This option
195 specifies the path, type and size of each key. See
196 <citerefentry><refentrytitle>ssh-keygen</refentrytitle>
197 <manvolnum>1</manvolnum></citerefentry> for supported types
198 and sizes.
199 '';
200 };
201
202 authorizedKeysFiles = mkOption {
203 type = types.listOf types.str;
204 default = [];
205 description = "Files from with authorized keys are read.";
206 };
207
208 extraConfig = mkOption {
209 type = types.lines;
210 default = "";
211 description = "Verbatim contents of <filename>sshd_config</filename>.";
212 };
213
214 knownHosts = mkOption {
215 default = {};
216 type = types.loaOf types.optionSet;
217 description = ''
218 The set of system-wide known SSH hosts.
219 '';
220 example = [
221 {
222 hostNames = [ "myhost" "myhost.mydomain.com" "10.10.1.4" ];
223 publicKeyFile = literalExample "./pubkeys/myhost_ssh_host_dsa_key.pub";
224 }
225 {
226 hostNames = [ "myhost2" ];
227 publicKeyFile = literalExample "./pubkeys/myhost2_ssh_host_dsa_key.pub";
228 }
229 ];
230 options = {
231 hostNames = mkOption {
232 type = types.listOf types.str;
233 default = [];
234 description = ''
235 A list of host names and/or IP numbers used for accessing
236 the host's ssh service.
237 '';
238 };
239 publicKey = mkOption {
240 default = null;
241 type = types.nullOr types.str;
242 example = "ecdsa-sha2-nistp521 AAAAE2VjZHN...UEPg==";
243 description = ''
244 The public key data for the host. You can fetch a public key
245 from a running SSH server with the <command>ssh-keyscan</command>
246 command. The public key should not include any host names, only
247 the key type and the key itself.
248 '';
249 };
250 publicKeyFile = mkOption {
251 default = null;
252 type = types.nullOr types.path;
253 description = ''
254 The path to the public key file for the host. The public
255 key file is read at build time and saved in the Nix store.
256 You can fetch a public key file from a running SSH server
257 with the <command>ssh-keyscan</command> command. The content
258 of the file should follow the same format as described for
259 the <literal>publicKey</literal> option.
260 '';
261 };
262 };
263 };
264
265 moduliFile = mkOption {
266 example = "services.openssh.moduliFile = /etc/my-local-ssh-moduli;";
267 type = types.path;
268 description = ''
269 Path to <literal>moduli</literal> file to install in
270 <literal>/etc/ssh/moduli</literal>. If this option is unset, then
271 the <literal>moduli</literal> file shipped with OpenSSH will be used.
272 '';
273 };
274
275 };
276
277 users.extraUsers = mkOption {
278 options = [ userOptions ];
279 };
280
281 };
282
283
284 ###### implementation
285
286 config = mkIf cfg.enable {
287
288 users.extraUsers.sshd =
289 { isSystemUser = true;
290 description = "SSH privilege separation user";
291 };
292
293 services.openssh.moduliFile = mkDefault "${cfgc.package}/etc/ssh/moduli";
294
295 environment.etc = authKeysFiles ++ [
296 { source = cfg.moduliFile;
297 target = "ssh/moduli";
298 }
299 { text = knownHostsText;
300 target = "ssh/ssh_known_hosts";
301 }
302 ];
303
304 systemd =
305 let
306 service =
307 { description = "SSH Daemon";
308
309 wantedBy = optional (!cfg.startWhenNeeded) "multi-user.target";
310
311 stopIfChanged = false;
312
313 path = [ cfgc.package pkgs.gawk ];
314
315 environment.LD_LIBRARY_PATH = nssModulesPath;
316
317 preStart =
318 ''
319 mkdir -m 0755 -p /etc/ssh
320
321 ${flip concatMapStrings cfg.hostKeys (k: ''
322 if ! [ -f "${k.path}" ]; then
323 ssh-keygen -t "${k.type}" ${if k ? bits then "-b ${toString k.bits}" else ""} -f "${k.path}" -N ""
324 fi
325 '')}
326 '';
327
328 serviceConfig =
329 { ExecStart =
330 "${cfgc.package}/sbin/sshd " + (optionalString cfg.startWhenNeeded "-i ") +
331 "-f ${pkgs.writeText "sshd_config" cfg.extraConfig}";
332 KillMode = "process";
333 } // (if cfg.startWhenNeeded then {
334 StandardInput = "socket";
335 } else {
336 Restart = "always";
337 Type = "forking";
338 PIDFile = "/run/sshd.pid";
339 });
340 };
341 in
342
343 if cfg.startWhenNeeded then {
344
345 sockets.sshd =
346 { description = "SSH Socket";
347 wantedBy = [ "sockets.target" ];
348 socketConfig.ListenStream = cfg.ports;
349 socketConfig.Accept = true;
350 };
351
352 services."sshd@" = service;
353
354 } else {
355
356 services.sshd = service;
357
358 };
359
360 networking.firewall.allowedTCPPorts = cfg.ports;
361
362 security.pam.services.sshd =
363 { startSession = true;
364 showMotd = true;
365 unixAuth = cfg.passwordAuthentication;
366 };
367
368 services.openssh.authorizedKeysFiles =
369 [ ".ssh/authorized_keys" ".ssh/authorized_keys2" "/etc/ssh/authorized_keys.d/%u" ];
370
371 services.openssh.extraConfig =
372 ''
373 PidFile /run/sshd.pid
374
375 Protocol 2
376
377 UsePAM yes
378
379 UsePrivilegeSeparation sandbox
380
381 AddressFamily ${if config.networking.enableIPv6 then "any" else "inet"}
382 ${concatMapStrings (port: ''
383 Port ${toString port}
384 '') cfg.ports}
385
386 ${concatMapStrings ({ port, addr, ... }: ''
387 ListenAddress ${addr}${if port != null then ":" + toString port else ""}
388 '') cfg.listenAddresses}
389
390 ${optionalString cfgc.setXAuthLocation ''
391 XAuthLocation ${pkgs.xorg.xauth}/bin/xauth
392 ''}
393
394 ${if cfg.forwardX11 then ''
395 X11Forwarding yes
396 '' else ''
397 X11Forwarding no
398 ''}
399
400 ${optionalString cfg.allowSFTP ''
401 Subsystem sftp ${cfgc.package}/libexec/sftp-server
402 ''}
403
404 PermitRootLogin ${cfg.permitRootLogin}
405 GatewayPorts ${cfg.gatewayPorts}
406 PasswordAuthentication ${if cfg.passwordAuthentication then "yes" else "no"}
407 ChallengeResponseAuthentication ${if cfg.challengeResponseAuthentication then "yes" else "no"}
408
409 PrintMotd no # handled by pam_motd
410
411 AuthorizedKeysFile ${toString cfg.authorizedKeysFiles}
412
413 ${flip concatMapStrings cfg.hostKeys (k: ''
414 HostKey ${k.path}
415 '')}
416 '';
417
418 assertions = [{ assertion = if cfg.forwardX11 then cfgc.setXAuthLocation else true;
419 message = "cannot enable X11 forwarding without setting xauth location";}]
420 ++ flip mapAttrsToList cfg.knownHosts (name: data: {
421 assertion = (data.publicKey == null && data.publicKeyFile != null) ||
422 (data.publicKey != null && data.publicKeyFile == null);
423 message = "knownHost ${name} must contain either a publicKey or publicKeyFile";
424 })
425 ++ flip map cfg.listenAddresses ({ addr, port, ... }: {
426 assertion = addr != null;
427 message = "addr must be specified in each listenAddresses entry";
428 });
429
430 };
431
432}