1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8with lib;
9
10let
11
12 cfg = config.boot.initrd.network.ssh;
13 shell = if cfg.shell == null then "/bin/ash" else cfg.shell;
14 inherit (config.programs.ssh) package;
15
16 enabled =
17 let
18 initrd = config.boot.initrd;
19 in
20 (initrd.network.enable || initrd.systemd.network.enable) && cfg.enable;
21
22in
23
24{
25
26 options.boot.initrd.network.ssh = {
27 enable = mkOption {
28 type = types.bool;
29 default = false;
30 description = ''
31 Start SSH service during initrd boot. It can be used to debug failing
32 boot on a remote server, enter pasphrase for an encrypted partition etc.
33 Service is killed when stage-1 boot is finished.
34
35 The sshd configuration is largely inherited from
36 {option}`services.openssh`.
37 '';
38 };
39
40 port = mkOption {
41 type = types.port;
42 default = 22;
43 description = ''
44 Port on which SSH initrd service should listen.
45 '';
46 };
47
48 shell = mkOption {
49 type = types.nullOr types.str;
50 default = null;
51 defaultText = ''"/bin/ash"'';
52 description = ''
53 Login shell of the remote user. Can be used to limit actions user can do.
54 '';
55 };
56
57 hostKeys = mkOption {
58 type = types.listOf (types.either types.str types.path);
59 default = [ ];
60 example = [
61 "/etc/secrets/initrd/ssh_host_rsa_key"
62 "/etc/secrets/initrd/ssh_host_ed25519_key"
63 ];
64 description = ''
65 Specify SSH host keys to import into the initrd.
66
67 To generate keys, use
68 {manpage}`ssh-keygen(1)`
69 as root:
70
71 ```
72 ssh-keygen -t rsa -N "" -f /etc/secrets/initrd/ssh_host_rsa_key
73 ssh-keygen -t ed25519 -N "" -f /etc/secrets/initrd/ssh_host_ed25519_key
74 ```
75
76 ::: {.warning}
77 Unless your bootloader supports initrd secrets, these keys
78 are stored insecurely in the global Nix store. Do NOT use
79 your regular SSH host private keys for this purpose or
80 you'll expose them to regular users!
81
82 Additionally, even if your initrd supports secrets, if
83 you're using initrd SSH to unlock an encrypted disk then
84 using your regular host keys exposes the private keys on
85 your unencrypted boot partition.
86 :::
87 '';
88 };
89
90 ignoreEmptyHostKeys = mkOption {
91 type = types.bool;
92 default = false;
93 description = ''
94 Allow leaving {option}`config.boot.initrd.network.ssh.hostKeys` empty,
95 to deploy ssh host keys out of band.
96 '';
97 };
98
99 authorizedKeys = mkOption {
100 type = types.listOf types.str;
101 default = config.users.users.root.openssh.authorizedKeys.keys;
102 defaultText = literalExpression "config.users.users.root.openssh.authorizedKeys.keys";
103 description = ''
104 Authorized keys for the root user on initrd.
105 You can combine the `authorizedKeys` and `authorizedKeyFiles` options.
106 '';
107 example = [
108 "ssh-rsa AAAAB3NzaC1yc2etc/etc/etcjwrsh8e596z6J0l7 example@host"
109 "ssh-ed25519 AAAAC3NzaCetcetera/etceteraJZMfk3QPfQ foo@bar"
110 ];
111 };
112
113 authorizedKeyFiles = mkOption {
114 type = types.listOf types.path;
115 default = config.users.users.root.openssh.authorizedKeys.keyFiles;
116 defaultText = literalExpression "config.users.users.root.openssh.authorizedKeys.keyFiles";
117 description = ''
118 Authorized keys taken from files for the root user on initrd.
119 You can combine the `authorizedKeyFiles` and `authorizedKeys` options.
120 '';
121 };
122
123 extraConfig = mkOption {
124 type = types.lines;
125 default = "";
126 description = "Verbatim contents of {file}`sshd_config`.";
127 };
128 };
129
130 imports =
131 map
132 (
133 opt:
134 mkRemovedOptionModule
135 (
136 [
137 "boot"
138 "initrd"
139 "network"
140 "ssh"
141 ]
142 ++ [ opt ]
143 )
144 ''
145 The initrd SSH functionality now uses OpenSSH rather than Dropbear.
146
147 If you want to keep your existing initrd SSH host keys, convert them with
148 $ dropbearconvert dropbear openssh dropbear_host_$type_key ssh_host_$type_key
149 and then set options.boot.initrd.network.ssh.hostKeys.
150 ''
151 )
152 [
153 "hostRSAKey"
154 "hostDSSKey"
155 "hostECDSAKey"
156 ];
157
158 config =
159 let
160 # Nix complains if you include a store hash in initrd path names, so
161 # as an awful hack we drop the first character of the hash.
162 initrdKeyPath =
163 path:
164 if isString path then
165 path
166 else
167 let
168 name = builtins.baseNameOf path;
169 in
170 builtins.unsafeDiscardStringContext ("/etc/ssh/" + substring 1 (stringLength name) name);
171
172 sshdCfg = config.services.openssh;
173
174 sshdConfig =
175 ''
176 UsePAM no
177 Port ${toString cfg.port}
178
179 PasswordAuthentication no
180 AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys2 /etc/ssh/authorized_keys.d/%u
181 ChallengeResponseAuthentication no
182
183 ${flip concatMapStrings cfg.hostKeys (path: ''
184 HostKey ${initrdKeyPath path}
185 '')}
186
187 ''
188 + lib.optionalString (sshdCfg.settings.KexAlgorithms != null) ''
189 KexAlgorithms ${concatStringsSep "," sshdCfg.settings.KexAlgorithms}
190 ''
191 + lib.optionalString (sshdCfg.settings.Ciphers != null) ''
192 Ciphers ${concatStringsSep "," sshdCfg.settings.Ciphers}
193 ''
194 + lib.optionalString (sshdCfg.settings.Macs != null) ''
195 MACs ${concatStringsSep "," sshdCfg.settings.Macs}
196 ''
197 + ''
198
199 LogLevel ${sshdCfg.settings.LogLevel}
200
201 ${
202 if sshdCfg.settings.UseDns then
203 ''
204 UseDNS yes
205 ''
206 else
207 ''
208 UseDNS no
209 ''
210 }
211
212 ${optionalString (!config.boot.initrd.systemd.enable) ''
213 SshdAuthPath /bin/sshd-auth
214 SshdSessionPath /bin/sshd-session
215 ''}
216
217 ${cfg.extraConfig}
218 '';
219 in
220 mkIf enabled {
221 assertions = [
222 {
223 assertion = cfg.authorizedKeys != [ ] || cfg.authorizedKeyFiles != [ ];
224 message = "You should specify at least one authorized key for initrd SSH";
225 }
226
227 {
228 assertion = (cfg.hostKeys != [ ]) || cfg.ignoreEmptyHostKeys;
229 message = ''
230 You must now pre-generate the host keys for initrd SSH.
231 See the boot.initrd.network.ssh.hostKeys documentation
232 for instructions.
233 '';
234 }
235 ];
236
237 warnings = lib.optional (config.boot.initrd.systemd.enable && cfg.shell != null) ''
238 Please set 'boot.initrd.systemd.users.root.shell' instead of 'boot.initrd.network.ssh.shell'
239 '';
240
241 boot.initrd.extraUtilsCommands = mkIf (!config.boot.initrd.systemd.enable) ''
242 copy_bin_and_libs ${package}/bin/sshd
243 copy_bin_and_libs ${package}/libexec/sshd-auth
244 copy_bin_and_libs ${package}/libexec/sshd-session
245 cp -pv ${pkgs.glibc.out}/lib/libnss_files.so.* $out/lib
246 '';
247
248 boot.initrd.extraUtilsCommandsTest = mkIf (!config.boot.initrd.systemd.enable) ''
249 # sshd requires a host key to check config, so we pass in the test's
250 tmpkey="$(mktemp initrd-ssh-testkey.XXXXXXXXXX)"
251 cp "${../../../tests/initrd-network-ssh/ssh_host_ed25519_key}" "$tmpkey"
252 # keys from Nix store are world-readable, which sshd doesn't like
253 chmod 600 "$tmpkey"
254 echo -n ${escapeShellArg sshdConfig} |
255 $out/bin/sshd -t -f /dev/stdin \
256 -h "$tmpkey"
257 rm "$tmpkey"
258 '';
259
260 boot.initrd.network.postCommands = mkIf (!config.boot.initrd.systemd.enable) ''
261 echo '${shell}' > /etc/shells
262 echo 'root:x:0:0:root:/root:${shell}' > /etc/passwd
263 echo 'sshd:x:1:1:sshd:/var/empty:/bin/nologin' >> /etc/passwd
264 echo 'passwd: files' > /etc/nsswitch.conf
265
266 mkdir -p /var/log /var/empty
267 touch /var/log/lastlog
268
269 mkdir -p /etc/ssh
270 echo -n ${escapeShellArg sshdConfig} > /etc/ssh/sshd_config
271
272 echo "export PATH=$PATH" >> /etc/profile
273 echo "export LD_LIBRARY_PATH=$LD_LIBRARY_PATH" >> /etc/profile
274
275 mkdir -p /root/.ssh
276 ${concatStrings (
277 map (key: ''
278 echo ${escapeShellArg key} >> /root/.ssh/authorized_keys
279 '') cfg.authorizedKeys
280 )}
281 ${concatStrings (
282 map (keyFile: ''
283 cat ${keyFile} >> /root/.ssh/authorized_keys
284 '') cfg.authorizedKeyFiles
285 )}
286
287 ${flip concatMapStrings cfg.hostKeys (path: ''
288 # keys from Nix store are world-readable, which sshd doesn't like
289 chmod 0600 "${initrdKeyPath path}"
290 '')}
291
292 /bin/sshd -e
293 '';
294
295 boot.initrd.postMountCommands = mkIf (!config.boot.initrd.systemd.enable) ''
296 # Stop sshd cleanly before stage 2.
297 #
298 # If you want to keep it around to debug post-mount SSH issues,
299 # run `touch /.keep_sshd` (either from an SSH session or in
300 # another initrd hook like preDeviceCommands).
301 if ! [ -e /.keep_sshd ]; then
302 pkill -x sshd
303 fi
304 '';
305
306 boot.initrd.secrets = listToAttrs (
307 map (path: nameValuePair (initrdKeyPath path) path) cfg.hostKeys
308 );
309
310 # Systemd initrd stuff
311 boot.initrd.systemd = mkIf config.boot.initrd.systemd.enable {
312 users.sshd = {
313 uid = 1;
314 group = "sshd";
315 };
316 groups.sshd = {
317 gid = 1;
318 };
319
320 users.root.shell = mkIf (
321 config.boot.initrd.network.ssh.shell != null
322 ) config.boot.initrd.network.ssh.shell;
323
324 contents = {
325 "/etc/ssh/sshd_config".text = sshdConfig;
326 "/etc/ssh/authorized_keys.d/root".text = concatStringsSep "\n" (
327 config.boot.initrd.network.ssh.authorizedKeys
328 ++ (map (file: lib.fileContents file) config.boot.initrd.network.ssh.authorizedKeyFiles)
329 );
330 };
331 storePaths = [
332 "${package}/bin/sshd"
333 "${package}/libexec/sshd-auth"
334 "${package}/libexec/sshd-session"
335 ];
336
337 services.sshd = {
338 description = "SSH Daemon";
339 wantedBy = [ "initrd.target" ];
340 after = [
341 "network.target"
342 "initrd-nixos-copy-secrets.service"
343 ];
344 before = [ "shutdown.target" ];
345 conflicts = [ "shutdown.target" ];
346
347 # Keys from Nix store are world-readable, which sshd doesn't
348 # like. If this were a real nix store and not the initrd, we
349 # neither would nor could do this
350 preStart = flip concatMapStrings cfg.hostKeys (path: ''
351 /bin/chmod 0600 "${initrdKeyPath path}"
352 '');
353 unitConfig.DefaultDependencies = false;
354 serviceConfig = {
355 ExecStart = "${package}/bin/sshd -D -f /etc/ssh/sshd_config";
356 Type = "simple";
357 KillMode = "process";
358 Restart = "on-failure";
359 };
360 };
361 };
362
363 };
364
365}