1# Global configuration for the SSH client.
2
3{ config, lib, pkgs, ... }:
4
5with lib;
6
7let
8
9 cfg = config.programs.ssh;
10
11 askPassword = cfg.askPassword;
12
13 askPasswordWrapper = pkgs.writeScript "ssh-askpass-wrapper"
14 ''
15 #! ${pkgs.runtimeShell} -e
16 export DISPLAY="$(systemctl --user show-environment | ${pkgs.gnused}/bin/sed 's/^DISPLAY=\(.*\)/\1/; t; d')"
17 exec ${askPassword} "$@"
18 '';
19
20 knownHosts = map (h: getAttr h cfg.knownHosts) (attrNames cfg.knownHosts);
21
22 knownHostsText = (flip (concatMapStringsSep "\n") knownHosts
23 (h: assert h.hostNames != [];
24 optionalString h.certAuthority "@cert-authority " + concatStringsSep "," h.hostNames + " "
25 + (if h.publicKey != null then h.publicKey else readFile h.publicKeyFile)
26 )) + "\n";
27
28in
29{
30 ###### interface
31
32 options = {
33
34 programs.ssh = {
35
36 askPassword = mkOption {
37 type = types.str;
38 default = "${pkgs.x11_ssh_askpass}/libexec/x11-ssh-askpass";
39 defaultText = literalExpression ''"''${pkgs.x11_ssh_askpass}/libexec/x11-ssh-askpass"'';
40 description = "Program used by SSH to ask for passwords.";
41 };
42
43 forwardX11 = mkOption {
44 type = types.bool;
45 default = false;
46 description = ''
47 Whether to request X11 forwarding on outgoing connections by default.
48 This is useful for running graphical programs on the remote machine and have them display to your local X11 server.
49 Historically, this value has depended on the value used by the local sshd daemon, but there really isn't a relation between the two.
50 Note: there are some security risks to forwarding an X11 connection.
51 NixOS's X server is built with the SECURITY extension, which prevents some obvious attacks.
52 To enable or disable forwarding on a per-connection basis, see the -X and -x options to ssh.
53 The -Y option to ssh enables trusted forwarding, which bypasses the SECURITY extension.
54 '';
55 };
56
57 setXAuthLocation = mkOption {
58 type = types.bool;
59 description = ''
60 Whether to set the path to <command>xauth</command> for X11-forwarded connections.
61 This causes a dependency on X11 packages.
62 '';
63 };
64
65 pubkeyAcceptedKeyTypes = mkOption {
66 type = types.listOf types.str;
67 default = [];
68 example = [ "ssh-ed25519" "ssh-rsa" ];
69 description = ''
70 Specifies the key types that will be used for public key authentication.
71 '';
72 };
73
74 hostKeyAlgorithms = mkOption {
75 type = types.listOf types.str;
76 default = [];
77 example = [ "ssh-ed25519" "ssh-rsa" ];
78 description = ''
79 Specifies the host key algorithms that the client wants to use in order of preference.
80 '';
81 };
82
83 extraConfig = mkOption {
84 type = types.lines;
85 default = "";
86 description = ''
87 Extra configuration text prepended to <filename>ssh_config</filename>. Other generated
88 options will be added after a <code>Host *</code> pattern.
89 See <citerefentry><refentrytitle>ssh_config</refentrytitle><manvolnum>5</manvolnum></citerefentry>
90 for help.
91 '';
92 };
93
94 startAgent = mkOption {
95 type = types.bool;
96 default = false;
97 description = ''
98 Whether to start the OpenSSH agent when you log in. The OpenSSH agent
99 remembers private keys for you so that you don't have to type in
100 passphrases every time you make an SSH connection. Use
101 <command>ssh-add</command> to add a key to the agent.
102 '';
103 };
104
105 agentTimeout = mkOption {
106 type = types.nullOr types.str;
107 default = null;
108 example = "1h";
109 description = ''
110 How long to keep the private keys in memory. Use null to keep them forever.
111 '';
112 };
113
114 agentPKCS11Whitelist = mkOption {
115 type = types.nullOr types.str;
116 default = null;
117 example = literalExpression ''"''${pkgs.opensc}/lib/opensc-pkcs11.so"'';
118 description = ''
119 A pattern-list of acceptable paths for PKCS#11 shared libraries
120 that may be used with the -s option to ssh-add.
121 '';
122 };
123
124 package = mkOption {
125 type = types.package;
126 default = pkgs.openssh;
127 defaultText = literalExpression "pkgs.openssh";
128 description = ''
129 The package used for the openssh client and daemon.
130 '';
131 };
132
133 knownHosts = mkOption {
134 default = {};
135 type = types.attrsOf (types.submodule ({ name, ... }: {
136 options = {
137 certAuthority = mkOption {
138 type = types.bool;
139 default = false;
140 description = ''
141 This public key is an SSH certificate authority, rather than an
142 individual host's key.
143 '';
144 };
145 hostNames = mkOption {
146 type = types.listOf types.str;
147 default = [];
148 description = ''
149 A list of host names and/or IP numbers used for accessing
150 the host's ssh service.
151 '';
152 };
153 publicKey = mkOption {
154 default = null;
155 type = types.nullOr types.str;
156 example = "ecdsa-sha2-nistp521 AAAAE2VjZHN...UEPg==";
157 description = ''
158 The public key data for the host. You can fetch a public key
159 from a running SSH server with the <command>ssh-keyscan</command>
160 command. The public key should not include any host names, only
161 the key type and the key itself.
162 '';
163 };
164 publicKeyFile = mkOption {
165 default = null;
166 type = types.nullOr types.path;
167 description = ''
168 The path to the public key file for the host. The public
169 key file is read at build time and saved in the Nix store.
170 You can fetch a public key file from a running SSH server
171 with the <command>ssh-keyscan</command> command. The content
172 of the file should follow the same format as described for
173 the <literal>publicKey</literal> option.
174 '';
175 };
176 };
177 config = {
178 hostNames = mkDefault [ name ];
179 };
180 }));
181 description = ''
182 The set of system-wide known SSH hosts.
183 '';
184 example = literalExpression ''
185 {
186 myhost = {
187 hostNames = [ "myhost" "myhost.mydomain.com" "10.10.1.4" ];
188 publicKeyFile = ./pubkeys/myhost_ssh_host_dsa_key.pub;
189 };
190 myhost2 = {
191 hostNames = [ "myhost2" ];
192 publicKeyFile = ./pubkeys/myhost2_ssh_host_dsa_key.pub;
193 };
194 }
195 '';
196 };
197
198 kexAlgorithms = mkOption {
199 type = types.nullOr (types.listOf types.str);
200 default = null;
201 example = [ "curve25519-sha256@libssh.org" "diffie-hellman-group-exchange-sha256" ];
202 description = ''
203 Specifies the available KEX (Key Exchange) algorithms.
204 '';
205 };
206
207 ciphers = mkOption {
208 type = types.nullOr (types.listOf types.str);
209 default = null;
210 example = [ "chacha20-poly1305@openssh.com" "aes256-gcm@openssh.com" ];
211 description = ''
212 Specifies the ciphers allowed and their order of preference.
213 '';
214 };
215
216 macs = mkOption {
217 type = types.nullOr (types.listOf types.str);
218 default = null;
219 example = [ "hmac-sha2-512-etm@openssh.com" "hmac-sha1" ];
220 description = ''
221 Specifies the MAC (message authentication code) algorithms in order of preference. The MAC algorithm is used
222 for data integrity protection.
223 '';
224 };
225 };
226
227 };
228
229 config = {
230
231 programs.ssh.setXAuthLocation =
232 mkDefault (config.services.xserver.enable || config.programs.ssh.forwardX11 || config.services.openssh.forwardX11);
233
234 assertions =
235 [ { assertion = cfg.forwardX11 -> cfg.setXAuthLocation;
236 message = "cannot enable X11 forwarding without setting XAuth location";
237 }
238 ] ++ flip mapAttrsToList cfg.knownHosts (name: data: {
239 assertion = (data.publicKey == null && data.publicKeyFile != null) ||
240 (data.publicKey != null && data.publicKeyFile == null);
241 message = "knownHost ${name} must contain either a publicKey or publicKeyFile";
242 });
243
244 # SSH configuration. Slight duplication of the sshd_config
245 # generation in the sshd service.
246 environment.etc."ssh/ssh_config".text =
247 ''
248 # Custom options from `extraConfig`, to override generated options
249 ${cfg.extraConfig}
250
251 # Generated options from other settings
252 Host *
253 AddressFamily ${if config.networking.enableIPv6 then "any" else "inet"}
254
255 ${optionalString cfg.setXAuthLocation ''
256 XAuthLocation ${pkgs.xorg.xauth}/bin/xauth
257 ''}
258
259 ForwardX11 ${if cfg.forwardX11 then "yes" else "no"}
260
261 ${optionalString (cfg.pubkeyAcceptedKeyTypes != []) "PubkeyAcceptedKeyTypes ${concatStringsSep "," cfg.pubkeyAcceptedKeyTypes}"}
262 ${optionalString (cfg.hostKeyAlgorithms != []) "HostKeyAlgorithms ${concatStringsSep "," cfg.hostKeyAlgorithms}"}
263 ${optionalString (cfg.kexAlgorithms != null) "KexAlgorithms ${concatStringsSep "," cfg.kexAlgorithms}"}
264 ${optionalString (cfg.ciphers != null) "Ciphers ${concatStringsSep "," cfg.ciphers}"}
265 ${optionalString (cfg.macs != null) "MACs ${concatStringsSep "," cfg.macs}"}
266 '';
267
268 environment.etc."ssh/ssh_known_hosts".text = knownHostsText;
269
270 # FIXME: this should really be socket-activated for über-awesomeness.
271 systemd.user.services.ssh-agent = mkIf cfg.startAgent
272 { description = "SSH Agent";
273 wantedBy = [ "default.target" ];
274 unitConfig.ConditionUser = "!@system";
275 serviceConfig =
276 { ExecStartPre = "${pkgs.coreutils}/bin/rm -f %t/ssh-agent";
277 ExecStart =
278 "${cfg.package}/bin/ssh-agent " +
279 optionalString (cfg.agentTimeout != null) ("-t ${cfg.agentTimeout} ") +
280 optionalString (cfg.agentPKCS11Whitelist != null) ("-P ${cfg.agentPKCS11Whitelist} ") +
281 "-a %t/ssh-agent";
282 StandardOutput = "null";
283 Type = "forking";
284 Restart = "on-failure";
285 SuccessExitStatus = "0 2";
286 };
287 # Allow ssh-agent to ask for confirmation. This requires the
288 # unit to know about the user's $DISPLAY (via ‘systemctl
289 # import-environment’).
290 environment.SSH_ASKPASS = optionalString config.services.xserver.enable askPasswordWrapper;
291 environment.DISPLAY = "fake"; # required to make ssh-agent start $SSH_ASKPASS
292 };
293
294 environment.extraInit = optionalString cfg.startAgent
295 ''
296 if [ -z "$SSH_AUTH_SOCK" -a -n "$XDG_RUNTIME_DIR" ]; then
297 export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/ssh-agent"
298 fi
299 '';
300
301 environment.variables.SSH_ASKPASS = optionalString config.services.xserver.enable askPassword;
302
303 };
304}