1# Global configuration for the SSH client.
2
3{
4 config,
5 lib,
6 pkgs,
7 ...
8}:
9
10let
11
12 cfg = config.programs.ssh;
13
14 askPasswordWrapper = pkgs.writeScript "ssh-askpass-wrapper" ''
15 #! ${pkgs.runtimeShell} -e
16 eval export $(systemctl --user show-environment | ${lib.getExe pkgs.gnugrep} -E '^(DISPLAY|WAYLAND_DISPLAY|XAUTHORITY)=')
17 exec ${cfg.askPassword} "$@"
18 '';
19
20 knownHosts = builtins.attrValues cfg.knownHosts;
21
22 knownHostsText =
23 (lib.flip (lib.concatMapStringsSep "\n") knownHosts (
24 h:
25 assert h.hostNames != [ ];
26 lib.optionalString h.certAuthority "@cert-authority "
27 + builtins.concatStringsSep "," h.hostNames
28 + " "
29 + (if h.publicKey != null then h.publicKey else builtins.readFile h.publicKeyFile)
30 ))
31 + "\n";
32
33 knownHostsFiles = [
34 "/etc/ssh/ssh_known_hosts"
35 ] ++ builtins.map pkgs.copyPathToStore cfg.knownHostsFiles;
36
37in
38{
39 ###### interface
40
41 options = {
42
43 programs.ssh = {
44
45 enableAskPassword = lib.mkOption {
46 type = lib.types.bool;
47 default = config.services.xserver.enable;
48 defaultText = lib.literalExpression "config.services.xserver.enable";
49 description = "Whether to configure SSH_ASKPASS in the environment.";
50 };
51
52 systemd-ssh-proxy.enable = lib.mkOption {
53 type = lib.types.bool;
54 default = true;
55 description = ''
56 Whether to enable systemd's ssh proxy plugin.
57 See {manpage}`systemd-ssh-proxy(1)`.
58 '';
59 };
60
61 askPassword = lib.mkOption {
62 type = lib.types.str;
63 default = "${pkgs.x11_ssh_askpass}/libexec/x11-ssh-askpass";
64 defaultText = lib.literalExpression ''"''${pkgs.x11_ssh_askpass}/libexec/x11-ssh-askpass"'';
65 description = "Program used by SSH to ask for passwords.";
66 };
67
68 forwardX11 = lib.mkOption {
69 type = with lib.types; nullOr bool;
70 default = false;
71 description = ''
72 Whether to request X11 forwarding on outgoing connections by default.
73 If set to null, the option is not set at all.
74 This is useful for running graphical programs on the remote machine and have them display to your local X11 server.
75 Historically, this value has depended on the value used by the local sshd daemon, but there really isn't a relation between the two.
76 Note: there are some security risks to forwarding an X11 connection.
77 NixOS's X server is built with the SECURITY extension, which prevents some obvious attacks.
78 To enable or disable forwarding on a per-connection basis, see the -X and -x options to ssh.
79 The -Y option to ssh enables trusted forwarding, which bypasses the SECURITY extension.
80 '';
81 };
82
83 setXAuthLocation = lib.mkOption {
84 type = lib.types.bool;
85 description = ''
86 Whether to set the path to {command}`xauth` for X11-forwarded connections.
87 This causes a dependency on X11 packages.
88 '';
89 };
90
91 pubkeyAcceptedKeyTypes = lib.mkOption {
92 type = lib.types.listOf lib.types.str;
93 default = [ ];
94 example = [
95 "ssh-ed25519"
96 "ssh-rsa"
97 ];
98 description = ''
99 Specifies the key lib.types that will be used for public key authentication.
100 '';
101 };
102
103 hostKeyAlgorithms = lib.mkOption {
104 type = lib.types.listOf lib.types.str;
105 default = [ ];
106 example = [
107 "ssh-ed25519"
108 "ssh-rsa"
109 ];
110 description = ''
111 Specifies the host key algorithms that the client wants to use in order of preference.
112 '';
113 };
114
115 extraConfig = lib.mkOption {
116 type = lib.types.lines;
117 default = "";
118 description = ''
119 Extra configuration text prepended to {file}`ssh_config`. Other generated
120 options will be added after a `Host *` pattern.
121 See {manpage}`ssh_config(5)`
122 for help.
123 '';
124 };
125
126 startAgent = lib.mkOption {
127 type = lib.types.bool;
128 default = false;
129 description = ''
130 Whether to start the OpenSSH agent when you log in. The OpenSSH agent
131 remembers private keys for you so that you don't have to type in
132 passphrases every time you make an SSH connection. Use
133 {command}`ssh-add` to add a key to the agent.
134 '';
135 };
136
137 agentTimeout = lib.mkOption {
138 type = lib.types.nullOr lib.types.str;
139 default = null;
140 example = "1h";
141 description = ''
142 How long to keep the private keys in memory. Use null to keep them forever.
143 '';
144 };
145
146 agentPKCS11Whitelist = lib.mkOption {
147 type = lib.types.nullOr lib.types.str;
148 default = null;
149 example = lib.literalExpression ''"''${pkgs.opensc}/lib/opensc-pkcs11.so"'';
150 description = ''
151 A pattern-list of acceptable paths for PKCS#11 shared libraries
152 that may be used with the -s option to ssh-add.
153 '';
154 };
155
156 package = lib.mkPackageOption pkgs "openssh" { };
157
158 knownHosts = lib.mkOption {
159 default = { };
160 type = lib.types.attrsOf (
161 lib.types.submodule (
162 {
163 name,
164 config,
165 options,
166 ...
167 }:
168 {
169 options = {
170 certAuthority = lib.mkOption {
171 type = lib.types.bool;
172 default = false;
173 description = ''
174 This public key is an SSH certificate authority, rather than an
175 individual host's key.
176 '';
177 };
178 hostNames = lib.mkOption {
179 type = lib.types.listOf lib.types.str;
180 default = [ name ] ++ config.extraHostNames;
181 defaultText = lib.literalExpression "[ ${name} ] ++ config.${options.extraHostNames}";
182 description = ''
183 A list of host names and/or IP numbers used for accessing
184 the host's ssh service. This list includes the name of the
185 containing `knownHosts` attribute by default
186 for convenience. If you wish to configure multiple host keys
187 for the same host use multiple `knownHosts`
188 entries with different attribute names and the same
189 `hostNames` list.
190 '';
191 };
192 extraHostNames = lib.mkOption {
193 type = lib.types.listOf lib.types.str;
194 default = [ ];
195 description = ''
196 A list of additional host names and/or IP numbers used for
197 accessing the host's ssh service. This list is ignored if
198 `hostNames` is set explicitly.
199 '';
200 };
201 publicKey = lib.mkOption {
202 default = null;
203 type = lib.types.nullOr lib.types.str;
204 example = "ecdsa-sha2-nistp521 AAAAE2VjZHN...UEPg==";
205 description = ''
206 The public key data for the host. You can fetch a public key
207 from a running SSH server with the {command}`ssh-keyscan`
208 command. The public key should not include any host names, only
209 the key type and the key itself.
210 '';
211 };
212 publicKeyFile = lib.mkOption {
213 default = null;
214 type = lib.types.nullOr lib.types.path;
215 description = ''
216 The path to the public key file for the host. The public
217 key file is read at build time and saved in the Nix store.
218 You can fetch a public key file from a running SSH server
219 with the {command}`ssh-keyscan` command. The content
220 of the file should follow the same format as described for
221 the `publicKey` option. Only a single key
222 is supported. If a host has multiple keys, use
223 {option}`programs.ssh.knownHostsFiles` instead.
224 '';
225 };
226 };
227 }
228 )
229 );
230 description = ''
231 The set of system-wide known SSH hosts. To make simple setups more
232 convenient the name of an attribute in this set is used as a host name
233 for the entry. This behaviour can be disabled by setting
234 `hostNames` explicitly. You can use
235 `extraHostNames` to add additional host names without
236 disabling this default.
237 '';
238 example = lib.literalExpression ''
239 {
240 myhost = {
241 extraHostNames = [ "myhost.mydomain.com" "10.10.1.4" ];
242 publicKeyFile = ./pubkeys/myhost_ssh_host_dsa_key.pub;
243 };
244 "myhost2.net".publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILIRuJ8p1Fi+m6WkHV0KWnRfpM1WxoW8XAS+XvsSKsTK";
245 "myhost2.net/dsa" = {
246 hostNames = [ "myhost2.net" ];
247 publicKeyFile = ./pubkeys/myhost2_ssh_host_dsa_key.pub;
248 };
249 }
250 '';
251 };
252
253 knownHostsFiles = lib.mkOption {
254 default = [ ];
255 type = with lib.types; listOf path;
256 description = ''
257 Files containing SSH host keys to set as global known hosts.
258 `/etc/ssh/ssh_known_hosts` (which is
259 generated by {option}`programs.ssh.knownHosts`) is
260 always included.
261 '';
262 example = lib.literalExpression ''
263 [
264 ./known_hosts
265 (writeText "github.keys" '''
266 github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=
267 github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=
268 github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl
269 ''')
270 ]
271 '';
272 };
273
274 kexAlgorithms = lib.mkOption {
275 type = lib.types.nullOr (lib.types.listOf lib.types.str);
276 default = null;
277 example = [
278 "curve25519-sha256@libssh.org"
279 "diffie-hellman-group-exchange-sha256"
280 ];
281 description = ''
282 Specifies the available KEX (Key Exchange) algorithms.
283 '';
284 };
285
286 ciphers = lib.mkOption {
287 type = lib.types.nullOr (lib.types.listOf lib.types.str);
288 default = null;
289 example = [
290 "chacha20-poly1305@openssh.com"
291 "aes256-gcm@openssh.com"
292 ];
293 description = ''
294 Specifies the ciphers allowed and their order of preference.
295 '';
296 };
297
298 macs = lib.mkOption {
299 type = lib.types.nullOr (lib.types.listOf lib.types.str);
300 default = null;
301 example = [
302 "hmac-sha2-512-etm@openssh.com"
303 "hmac-sha1"
304 ];
305 description = ''
306 Specifies the MAC (message authentication code) algorithms in order of preference. The MAC algorithm is used
307 for data integrity protection.
308 '';
309 };
310 };
311
312 };
313
314 config = {
315
316 programs.ssh.setXAuthLocation = lib.mkDefault (
317 config.services.xserver.enable
318 || config.programs.ssh.forwardX11 == true
319 || config.services.openssh.settings.X11Forwarding
320 );
321
322 assertions =
323 [
324 {
325 assertion = cfg.forwardX11 == true -> cfg.setXAuthLocation;
326 message = "cannot enable X11 forwarding without setting XAuth location";
327 }
328 ]
329 ++ lib.flip lib.mapAttrsToList cfg.knownHosts (
330 name: data: {
331 assertion =
332 (data.publicKey == null && data.publicKeyFile != null)
333 || (data.publicKey != null && data.publicKeyFile == null);
334 message = "knownHost ${name} must contain either a publicKey or publicKeyFile";
335 }
336 );
337
338 # SSH configuration. Slight duplication of the sshd_config
339 # generation in the sshd service.
340 environment.etc."ssh/ssh_config".text = ''
341 # Custom options from `extraConfig`, to override generated options
342 ${cfg.extraConfig}
343
344 # Generated options from other settings
345 Host *
346 ${lib.optionalString cfg.systemd-ssh-proxy.enable ''
347 # See systemd-ssh-proxy(1)
348 Include ${config.systemd.package}/lib/systemd/ssh_config.d/20-systemd-ssh-proxy.conf
349 ''}
350
351 GlobalKnownHostsFile ${builtins.concatStringsSep " " knownHostsFiles}
352
353 ${lib.optionalString (!config.networking.enableIPv6) "AddressFamily inet"}
354 ${lib.optionalString cfg.setXAuthLocation "XAuthLocation ${pkgs.xorg.xauth}/bin/xauth"}
355 ${lib.optionalString (cfg.forwardX11 != null)
356 "ForwardX11 ${if cfg.forwardX11 then "yes" else "no"}"
357 }
358
359 ${lib.optionalString (
360 cfg.pubkeyAcceptedKeyTypes != [ ]
361 ) "PubkeyAcceptedKeyTypes ${builtins.concatStringsSep "," cfg.pubkeyAcceptedKeyTypes}"}
362 ${lib.optionalString (
363 cfg.hostKeyAlgorithms != [ ]
364 ) "HostKeyAlgorithms ${builtins.concatStringsSep "," cfg.hostKeyAlgorithms}"}
365 ${lib.optionalString (
366 cfg.kexAlgorithms != null
367 ) "KexAlgorithms ${builtins.concatStringsSep "," cfg.kexAlgorithms}"}
368 ${lib.optionalString (cfg.ciphers != null) "Ciphers ${builtins.concatStringsSep "," cfg.ciphers}"}
369 ${lib.optionalString (cfg.macs != null) "MACs ${builtins.concatStringsSep "," cfg.macs}"}
370 '';
371
372 environment.etc."ssh/ssh_known_hosts".text = knownHostsText;
373
374 # FIXME: this should really be socket-activated for über-awesomeness.
375 systemd.user.services.ssh-agent = lib.mkIf cfg.startAgent {
376 description = "SSH Agent";
377 wantedBy = [ "default.target" ];
378 unitConfig.ConditionUser = "!@system";
379 serviceConfig = {
380 ExecStartPre = "${pkgs.coreutils}/bin/rm -f %t/ssh-agent";
381 ExecStart =
382 "${cfg.package}/bin/ssh-agent "
383 + lib.optionalString (cfg.agentTimeout != null) ("-t ${cfg.agentTimeout} ")
384 + lib.optionalString (cfg.agentPKCS11Whitelist != null) ("-P ${cfg.agentPKCS11Whitelist} ")
385 + "-a %t/ssh-agent";
386 StandardOutput = "null";
387 Type = "forking";
388 Restart = "on-failure";
389 SuccessExitStatus = "0 2";
390 };
391 # Allow ssh-agent to ask for confirmation. This requires the
392 # unit to know about the user's $DISPLAY (via ‘systemctl
393 # import-environment’).
394 environment.SSH_ASKPASS = lib.optionalString cfg.enableAskPassword askPasswordWrapper;
395 environment.DISPLAY = "fake"; # required to make ssh-agent start $SSH_ASKPASS
396 };
397
398 environment.extraInit = lib.optionalString cfg.startAgent ''
399 if [ -z "$SSH_AUTH_SOCK" -a -n "$XDG_RUNTIME_DIR" ]; then
400 export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/ssh-agent"
401 fi
402 '';
403
404 environment.variables.SSH_ASKPASS = lib.optionalString cfg.enableAskPassword cfg.askPassword;
405
406 };
407}