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 with 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 "${cfgc.package}/sbin/sshd " + (optionalString cfg.startWhenNeeded "-i ") +
267 "-f ${pkgs.writeText "sshd_config" cfg.extraConfig}";
268 KillMode = "process";
269 } // (if cfg.startWhenNeeded then {
270 StandardInput = "socket";
271 } else {
272 Restart = "always";
273 Type = "forking";
274 PIDFile = "/run/sshd.pid";
275 });
276 };
277 in
278
279 if cfg.startWhenNeeded then {
280
281 sockets.sshd =
282 { description = "SSH Socket";
283 wantedBy = [ "sockets.target" ];
284 socketConfig.ListenStream = cfg.ports;
285 socketConfig.Accept = true;
286 };
287
288 services."sshd@" = service;
289
290 } else {
291
292 services.sshd = service;
293
294 };
295
296 networking.firewall.allowedTCPPorts = cfg.ports;
297
298 security.pam.services.sshd =
299 { startSession = true;
300 showMotd = true;
301 unixAuth = cfg.passwordAuthentication;
302 };
303
304 services.openssh.authorizedKeysFiles =
305 [ ".ssh/authorized_keys" ".ssh/authorized_keys2" "/etc/ssh/authorized_keys.d/%u" ];
306
307 services.openssh.extraConfig = mkOrder 0
308 ''
309 PidFile /run/sshd.pid
310
311 Protocol 2
312
313 UsePAM yes
314
315 UsePrivilegeSeparation sandbox
316
317 AddressFamily ${if config.networking.enableIPv6 then "any" else "inet"}
318 ${concatMapStrings (port: ''
319 Port ${toString port}
320 '') cfg.ports}
321
322 ${concatMapStrings ({ port, addr, ... }: ''
323 ListenAddress ${addr}${if port != null then ":" + toString port else ""}
324 '') cfg.listenAddresses}
325
326 ${optionalString cfgc.setXAuthLocation ''
327 XAuthLocation ${pkgs.xorg.xauth}/bin/xauth
328 ''}
329
330 ${if cfg.forwardX11 then ''
331 X11Forwarding yes
332 '' else ''
333 X11Forwarding no
334 ''}
335
336 ${optionalString cfg.allowSFTP ''
337 Subsystem sftp ${cfgc.package}/libexec/sftp-server
338 ''}
339
340 PermitRootLogin ${cfg.permitRootLogin}
341 GatewayPorts ${cfg.gatewayPorts}
342 PasswordAuthentication ${if cfg.passwordAuthentication then "yes" else "no"}
343 ChallengeResponseAuthentication ${if cfg.challengeResponseAuthentication then "yes" else "no"}
344
345 PrintMotd no # handled by pam_motd
346
347 AuthorizedKeysFile ${toString cfg.authorizedKeysFiles}
348
349 ${flip concatMapStrings cfg.hostKeys (k: ''
350 HostKey ${k.path}
351 '')}
352
353 # Allow DSA client keys for now. (These were deprecated
354 # in OpenSSH 7.0.)
355 PubkeyAcceptedKeyTypes +ssh-dss
356
357 # Re-enable DSA host keys for now.
358 ${optionalString supportOldHostKeys ''
359 HostKeyAlgorithms +ssh-dss
360 ''}
361 '';
362
363 assertions = [{ assertion = if cfg.forwardX11 then cfgc.setXAuthLocation else true;
364 message = "cannot enable X11 forwarding without setting xauth location";}]
365 ++ flip map cfg.listenAddresses ({ addr, port, ... }: {
366 assertion = addr != null;
367 message = "addr must be specified in each listenAddresses entry";
368 });
369
370 };
371
372}