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