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 '';
25 };
26
27 keyFiles = mkOption {
28 type = types.listOf types.path;
29 default = [];
30 description = ''
31 A list of files each containing one OpenSSH public key that should be
32 added to the user's authorized keys. The contents of the files are
33 read at build time and added to a file that the SSH daemon reads in
34 addition to the the user's authorized_keys file. You can combine the
35 <literal>keyFiles</literal> and <literal>keys</literal> options.
36 '';
37 };
38 };
39
40 };
41
42 authKeysFiles = let
43 mkAuthKeyFile = u: nameValuePair "ssh/authorized_keys.d/${u.name}" {
44 mode = "0444";
45 source = pkgs.writeText "${u.name}-authorized_keys" ''
46 ${concatStringsSep "\n" u.openssh.authorizedKeys.keys}
47 ${concatMapStrings (f: readFile f + "\n") u.openssh.authorizedKeys.keyFiles}
48 '';
49 };
50 usersWithKeys = attrValues (flip filterAttrs config.users.extraUsers (n: u:
51 length u.openssh.authorizedKeys.keys != 0 || length u.openssh.authorizedKeys.keyFiles != 0
52 ));
53 in listToAttrs (map mkAuthKeyFile usersWithKeys);
54
55 supportOldHostKeys = !versionAtLeast config.system.stateVersion "15.07";
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 = cfgc.setXAuthLocation;
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 permitRootLogin = mkOption {
105 default = "without-password";
106 type = types.enum ["yes" "without-password" "forced-commands-only" "no"];
107 description = ''
108 Whether the root user can login using ssh.
109 '';
110 };
111
112 gatewayPorts = mkOption {
113 type = types.str;
114 default = "no";
115 description = ''
116 Specifies whether remote hosts are allowed to connect to
117 ports forwarded for the client. See
118 <citerefentry><refentrytitle>sshd_config</refentrytitle>
119 <manvolnum>5</manvolnum></citerefentry>.
120 '';
121 };
122
123 ports = mkOption {
124 type = types.listOf types.int;
125 default = [22];
126 description = ''
127 Specifies on which ports the SSH daemon listens.
128 '';
129 };
130
131 listenAddresses = mkOption {
132 type = types.listOf types.optionSet;
133 default = [];
134 example = [ { addr = "192.168.3.1"; port = 22; } { addr = "0.0.0.0"; port = 64022; } ];
135 description = ''
136 List of addresses and ports to listen on (ListenAddress directive
137 in config). If port is not specified for address sshd will listen
138 on all ports specified by <literal>ports</literal> option.
139 NOTE: this will override default listening on all local addresses and port 22.
140 NOTE: setting this option won't automatically enable given ports
141 in firewall configuration.
142 '';
143 options = {
144 addr = mkOption {
145 type = types.nullOr types.str;
146 default = null;
147 description = ''
148 Host, IPv4 or IPv6 address to listen to.
149 '';
150 };
151 port = mkOption {
152 type = types.nullOr types.int;
153 default = null;
154 description = ''
155 Port to listen to.
156 '';
157 };
158 };
159 };
160
161 passwordAuthentication = mkOption {
162 type = types.bool;
163 default = true;
164 description = ''
165 Specifies whether password authentication is allowed.
166 '';
167 };
168
169 challengeResponseAuthentication = mkOption {
170 type = types.bool;
171 default = true;
172 description = ''
173 Specifies whether challenge/response authentication is allowed.
174 '';
175 };
176
177 hostKeys = mkOption {
178 type = types.listOf types.attrs;
179 default =
180 [ { type = "rsa"; bits = 4096; path = "/etc/ssh/ssh_host_rsa_key"; }
181 { type = "ed25519"; path = "/etc/ssh/ssh_host_ed25519_key"; }
182 ] ++ optionals supportOldHostKeys
183 [ { type = "dsa"; path = "/etc/ssh/ssh_host_dsa_key"; }
184 { type = "ecdsa"; bits = 521; path = "/etc/ssh/ssh_host_ecdsa_key"; }
185 ];
186 description = ''
187 NixOS can automatically generate SSH host keys. This option
188 specifies the path, type and size of each key. See
189 <citerefentry><refentrytitle>ssh-keygen</refentrytitle>
190 <manvolnum>1</manvolnum></citerefentry> for supported types
191 and sizes.
192 '';
193 };
194
195 authorizedKeysFiles = mkOption {
196 type = types.listOf types.str;
197 default = [];
198 description = "Files from which authorized keys are read.";
199 };
200
201 extraConfig = mkOption {
202 type = types.lines;
203 default = "";
204 description = "Verbatim contents of <filename>sshd_config</filename>.";
205 };
206
207 moduliFile = mkOption {
208 example = "services.openssh.moduliFile = /etc/my-local-ssh-moduli;";
209 type = types.path;
210 description = ''
211 Path to <literal>moduli</literal> file to install in
212 <literal>/etc/ssh/moduli</literal>. If this option is unset, then
213 the <literal>moduli</literal> file shipped with OpenSSH will be used.
214 '';
215 };
216
217 };
218
219 users.users = mkOption {
220 options = [ userOptions ];
221 };
222
223 };
224
225
226 ###### implementation
227
228 config = mkIf cfg.enable {
229
230 users.extraUsers.sshd =
231 { isSystemUser = true;
232 description = "SSH privilege separation user";
233 };
234
235 services.openssh.moduliFile = mkDefault "${cfgc.package}/etc/ssh/moduli";
236
237 environment.etc = authKeysFiles //
238 { "ssh/moduli".source = cfg.moduliFile; };
239
240 systemd =
241 let
242 service =
243 { description = "SSH Daemon";
244
245 wantedBy = optional (!cfg.startWhenNeeded) "multi-user.target";
246
247 stopIfChanged = false;
248
249 path = [ cfgc.package pkgs.gawk ];
250
251 environment.LD_LIBRARY_PATH = nssModulesPath;
252
253 preStart =
254 ''
255 mkdir -m 0755 -p /etc/ssh
256
257 ${flip concatMapStrings cfg.hostKeys (k: ''
258 if ! [ -f "${k.path}" ]; then
259 ssh-keygen -t "${k.type}" ${if k ? bits then "-b ${toString k.bits}" else ""} -f "${k.path}" -N ""
260 fi
261 '')}
262 '';
263
264 serviceConfig =
265 { ExecStart =
266 (optionalString cfg.startWhenNeeded "-") +
267 "${cfgc.package}/bin/sshd " + (optionalString cfg.startWhenNeeded "-i ") +
268 "-f ${pkgs.writeText "sshd_config" cfg.extraConfig}";
269 KillMode = "process";
270 } // (if cfg.startWhenNeeded then {
271 StandardInput = "socket";
272 } else {
273 Restart = "always";
274 Type = "forking";
275 PIDFile = "/run/sshd.pid";
276 });
277 };
278 in
279
280 if cfg.startWhenNeeded then {
281
282 sockets.sshd =
283 { description = "SSH Socket";
284 wantedBy = [ "sockets.target" ];
285 socketConfig.ListenStream = cfg.ports;
286 socketConfig.Accept = true;
287 };
288
289 services."sshd@" = service;
290
291 } else {
292
293 services.sshd = service;
294
295 };
296
297 networking.firewall.allowedTCPPorts = cfg.ports;
298
299 security.pam.services.sshd =
300 { startSession = true;
301 showMotd = true;
302 unixAuth = cfg.passwordAuthentication;
303 };
304
305 services.openssh.authorizedKeysFiles =
306 [ ".ssh/authorized_keys" ".ssh/authorized_keys2" "/etc/ssh/authorized_keys.d/%u" ];
307
308 services.openssh.extraConfig = mkOrder 0
309 ''
310 PidFile /run/sshd.pid
311
312 Protocol 2
313
314 UsePAM yes
315
316 UsePrivilegeSeparation sandbox
317
318 AddressFamily ${if config.networking.enableIPv6 then "any" else "inet"}
319 ${concatMapStrings (port: ''
320 Port ${toString port}
321 '') cfg.ports}
322
323 ${concatMapStrings ({ port, addr, ... }: ''
324 ListenAddress ${addr}${if port != null then ":" + toString port else ""}
325 '') cfg.listenAddresses}
326
327 ${optionalString cfgc.setXAuthLocation ''
328 XAuthLocation ${pkgs.xorg.xauth}/bin/xauth
329 ''}
330
331 ${if cfg.forwardX11 then ''
332 X11Forwarding yes
333 '' else ''
334 X11Forwarding no
335 ''}
336
337 ${optionalString cfg.allowSFTP ''
338 Subsystem sftp ${cfgc.package}/libexec/sftp-server
339 ''}
340
341 PermitRootLogin ${cfg.permitRootLogin}
342 GatewayPorts ${cfg.gatewayPorts}
343 PasswordAuthentication ${if cfg.passwordAuthentication then "yes" else "no"}
344 ChallengeResponseAuthentication ${if cfg.challengeResponseAuthentication then "yes" else "no"}
345
346 PrintMotd no # handled by pam_motd
347
348 AuthorizedKeysFile ${toString cfg.authorizedKeysFiles}
349
350 ${flip concatMapStrings cfg.hostKeys (k: ''
351 HostKey ${k.path}
352 '')}
353
354 # Allow DSA client keys for now. (These were deprecated
355 # in OpenSSH 7.0.)
356 PubkeyAcceptedKeyTypes +ssh-dss
357
358 # Re-enable DSA host keys for now.
359 ${optionalString supportOldHostKeys ''
360 HostKeyAlgorithms +ssh-dss
361 ''}
362 '';
363
364 assertions = [{ assertion = if cfg.forwardX11 then cfgc.setXAuthLocation else true;
365 message = "cannot enable X11 forwarding without setting xauth location";}]
366 ++ flip map cfg.listenAddresses ({ addr, port, ... }: {
367 assertion = addr != null;
368 message = "addr must be specified in each listenAddresses entry";
369 });
370
371 };
372
373}