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
57 supportOldHostKeys = !versionAtLeast config.system.stateVersion "15.07";
58
59in
60
61{
62
63 ###### interface
64
65 options = {
66
67 services.openssh = {
68
69 enable = mkOption {
70 type = types.bool;
71 default = false;
72 description = ''
73 Whether to enable the OpenSSH secure shell daemon, which
74 allows secure remote logins.
75 '';
76 };
77
78 startWhenNeeded = mkOption {
79 type = types.bool;
80 default = false;
81 description = ''
82 If set, <command>sshd</command> is socket-activated; that
83 is, instead of having it permanently running as a daemon,
84 systemd will start an instance for each incoming connection.
85 '';
86 };
87
88 forwardX11 = mkOption {
89 type = types.bool;
90 default = false;
91 description = ''
92 Whether to allow X11 connections to be forwarded.
93 '';
94 };
95
96 allowSFTP = mkOption {
97 type = types.bool;
98 default = true;
99 description = ''
100 Whether to enable the SFTP subsystem in the SSH daemon. This
101 enables the use of commands such as <command>sftp</command> and
102 <command>sshfs</command>.
103 '';
104 };
105
106 permitRootLogin = mkOption {
107 default = "prohibit-password";
108 type = types.enum ["yes" "without-password" "prohibit-password" "forced-commands-only" "no"];
109 description = ''
110 Whether the root user can login using ssh.
111 '';
112 };
113
114 gatewayPorts = mkOption {
115 type = types.str;
116 default = "no";
117 description = ''
118 Specifies whether remote hosts are allowed to connect to
119 ports forwarded for the client. See
120 <citerefentry><refentrytitle>sshd_config</refentrytitle>
121 <manvolnum>5</manvolnum></citerefentry>.
122 '';
123 };
124
125 ports = mkOption {
126 type = types.listOf types.int;
127 default = [22];
128 description = ''
129 Specifies on which ports the SSH daemon listens.
130 '';
131 };
132
133 listenAddresses = mkOption {
134 type = with types; listOf (submodule {
135 options = {
136 addr = mkOption {
137 type = types.nullOr types.str;
138 default = null;
139 description = ''
140 Host, IPv4 or IPv6 address to listen to.
141 '';
142 };
143 port = mkOption {
144 type = types.nullOr types.int;
145 default = null;
146 description = ''
147 Port to listen to.
148 '';
149 };
150 };
151 });
152 default = [];
153 example = [ { addr = "192.168.3.1"; port = 22; } { addr = "0.0.0.0"; port = 64022; } ];
154 description = ''
155 List of addresses and ports to listen on (ListenAddress directive
156 in config). If port is not specified for address sshd will listen
157 on all ports specified by <literal>ports</literal> option.
158 NOTE: this will override default listening on all local addresses and port 22.
159 NOTE: setting this option won't automatically enable given ports
160 in firewall configuration.
161 '';
162 };
163
164 passwordAuthentication = mkOption {
165 type = types.bool;
166 default = true;
167 description = ''
168 Specifies whether password authentication is allowed.
169 '';
170 };
171
172 challengeResponseAuthentication = mkOption {
173 type = types.bool;
174 default = true;
175 description = ''
176 Specifies whether challenge/response authentication is allowed.
177 '';
178 };
179
180 hostKeys = mkOption {
181 type = types.listOf types.attrs;
182 default =
183 [ { type = "rsa"; bits = 4096; path = "/etc/ssh/ssh_host_rsa_key"; }
184 { type = "ed25519"; path = "/etc/ssh/ssh_host_ed25519_key"; }
185 ] ++ optionals supportOldHostKeys
186 [ { type = "dsa"; path = "/etc/ssh/ssh_host_dsa_key"; }
187 { type = "ecdsa"; bits = 521; path = "/etc/ssh/ssh_host_ecdsa_key"; }
188 ];
189 description = ''
190 NixOS can automatically generate SSH host keys. This option
191 specifies the path, type and size of each key. See
192 <citerefentry><refentrytitle>ssh-keygen</refentrytitle>
193 <manvolnum>1</manvolnum></citerefentry> for supported types
194 and sizes.
195 '';
196 };
197
198 authorizedKeysFiles = mkOption {
199 type = types.listOf types.str;
200 default = [];
201 description = "Files from which authorized keys are read.";
202 };
203
204 extraConfig = mkOption {
205 type = types.lines;
206 default = "";
207 description = "Verbatim contents of <filename>sshd_config</filename>.";
208 };
209
210 moduliFile = mkOption {
211 example = "services.openssh.moduliFile = /etc/my-local-ssh-moduli;";
212 type = types.path;
213 description = ''
214 Path to <literal>moduli</literal> file to install in
215 <literal>/etc/ssh/moduli</literal>. If this option is unset, then
216 the <literal>moduli</literal> file shipped with OpenSSH will be used.
217 '';
218 };
219
220 };
221
222 users.users = mkOption {
223 options = [ userOptions ];
224 };
225
226 };
227
228
229 ###### implementation
230
231 config = mkIf cfg.enable {
232
233 users.extraUsers.sshd =
234 { isSystemUser = true;
235 description = "SSH privilege separation user";
236 };
237
238 services.openssh.moduliFile = mkDefault "${cfgc.package}/etc/ssh/moduli";
239
240 environment.etc = authKeysFiles //
241 { "ssh/moduli".source = cfg.moduliFile; };
242
243 systemd =
244 let
245 service =
246 { description = "SSH Daemon";
247
248 wantedBy = optional (!cfg.startWhenNeeded) "multi-user.target";
249
250 stopIfChanged = false;
251
252 path = [ cfgc.package pkgs.gawk ];
253
254 environment.LD_LIBRARY_PATH = nssModulesPath;
255
256 preStart =
257 ''
258 # Make sure we don't write to stdout, since in case of
259 # socket activation, it goes to the remote side (#19589).
260 exec >&2
261
262 mkdir -m 0755 -p /etc/ssh
263
264 ${flip concatMapStrings cfg.hostKeys (k: ''
265 if ! [ -f "${k.path}" ]; then
266 ssh-keygen -t "${k.type}" ${if k ? bits then "-b ${toString k.bits}" else ""} -f "${k.path}" -N ""
267 fi
268 '')}
269 '';
270
271 serviceConfig =
272 { ExecStart =
273 (optionalString cfg.startWhenNeeded "-") +
274 "${cfgc.package}/bin/sshd " + (optionalString cfg.startWhenNeeded "-i ") +
275 "-f ${pkgs.writeText "sshd_config" cfg.extraConfig}";
276 KillMode = "process";
277 } // (if cfg.startWhenNeeded then {
278 StandardInput = "socket";
279 StandardError = "journal";
280 } else {
281 Restart = "always";
282 Type = "simple";
283 });
284 };
285 in
286
287 if cfg.startWhenNeeded then {
288
289 sockets.sshd =
290 { description = "SSH Socket";
291 wantedBy = [ "sockets.target" ];
292 socketConfig.ListenStream = cfg.ports;
293 socketConfig.Accept = true;
294 };
295
296 services."sshd@" = service;
297
298 } else {
299
300 services.sshd = service;
301
302 };
303
304 networking.firewall.allowedTCPPorts = cfg.ports;
305
306 security.pam.services.sshd =
307 { startSession = true;
308 showMotd = true;
309 unixAuth = cfg.passwordAuthentication;
310 };
311
312 services.openssh.authorizedKeysFiles =
313 [ ".ssh/authorized_keys" ".ssh/authorized_keys2" "/etc/ssh/authorized_keys.d/%u" ];
314
315 services.openssh.extraConfig = mkOrder 0
316 ''
317 Protocol 2
318
319 UsePAM yes
320
321 AddressFamily ${if config.networking.enableIPv6 then "any" else "inet"}
322 ${concatMapStrings (port: ''
323 Port ${toString port}
324 '') cfg.ports}
325
326 ${concatMapStrings ({ port, addr, ... }: ''
327 ListenAddress ${addr}${if port != null then ":" + toString port else ""}
328 '') cfg.listenAddresses}
329
330 ${optionalString cfgc.setXAuthLocation ''
331 XAuthLocation ${pkgs.xorg.xauth}/bin/xauth
332 ''}
333
334 ${if cfg.forwardX11 then ''
335 X11Forwarding yes
336 '' else ''
337 X11Forwarding no
338 ''}
339
340 ${optionalString cfg.allowSFTP ''
341 Subsystem sftp ${cfgc.package}/libexec/sftp-server
342 ''}
343
344 PermitRootLogin ${cfg.permitRootLogin}
345 GatewayPorts ${cfg.gatewayPorts}
346 PasswordAuthentication ${if cfg.passwordAuthentication then "yes" else "no"}
347 ChallengeResponseAuthentication ${if cfg.challengeResponseAuthentication then "yes" else "no"}
348
349 PrintMotd no # handled by pam_motd
350
351 AuthorizedKeysFile ${toString cfg.authorizedKeysFiles}
352
353 ${flip concatMapStrings cfg.hostKeys (k: ''
354 HostKey ${k.path}
355 '')}
356
357 # Allow DSA client keys for now. (These were deprecated
358 # in OpenSSH 7.0.)
359 PubkeyAcceptedKeyTypes +ssh-dss
360
361 # Re-enable DSA host keys for now.
362 ${optionalString supportOldHostKeys ''
363 HostKeyAlgorithms +ssh-dss
364 ''}
365 '';
366
367 assertions = [{ assertion = if cfg.forwardX11 then cfgc.setXAuthLocation else true;
368 message = "cannot enable X11 forwarding without setting xauth location";}]
369 ++ flip map cfg.listenAddresses ({ addr, port, ... }: {
370 assertion = addr != null;
371 message = "addr must be specified in each listenAddresses entry";
372 });
373
374 };
375
376}