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 UsePAM no
176 Port ${toString cfg.port}
177
178 PasswordAuthentication no
179 AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys2 /etc/ssh/authorized_keys.d/%u
180 ChallengeResponseAuthentication no
181
182 ${flip concatMapStrings cfg.hostKeys (path: ''
183 HostKey ${initrdKeyPath path}
184 '')}
185
186 ''
187 + lib.optionalString (sshdCfg.settings.KexAlgorithms != null) ''
188 KexAlgorithms ${concatStringsSep "," sshdCfg.settings.KexAlgorithms}
189 ''
190 + lib.optionalString (sshdCfg.settings.Ciphers != null) ''
191 Ciphers ${concatStringsSep "," sshdCfg.settings.Ciphers}
192 ''
193 + lib.optionalString (sshdCfg.settings.Macs != null) ''
194 MACs ${concatStringsSep "," sshdCfg.settings.Macs}
195 ''
196 + ''
197
198 LogLevel ${sshdCfg.settings.LogLevel}
199
200 ${
201 if sshdCfg.settings.UseDns then
202 ''
203 UseDNS yes
204 ''
205 else
206 ''
207 UseDNS no
208 ''
209 }
210
211 ${optionalString (!config.boot.initrd.systemd.enable) ''
212 SshdAuthPath /bin/sshd-auth
213 SshdSessionPath /bin/sshd-session
214 ''}
215
216 ${cfg.extraConfig}
217 '';
218 in
219 mkIf enabled {
220 assertions = [
221 {
222 assertion = cfg.authorizedKeys != [ ] || cfg.authorizedKeyFiles != [ ];
223 message = "You should specify at least one authorized key for initrd SSH";
224 }
225
226 {
227 assertion = (cfg.hostKeys != [ ]) || cfg.ignoreEmptyHostKeys;
228 message = ''
229 You must now pre-generate the host keys for initrd SSH.
230 See the boot.initrd.network.ssh.hostKeys documentation
231 for instructions.
232 '';
233 }
234 ];
235
236 warnings = lib.optional (config.boot.initrd.systemd.enable && cfg.shell != null) ''
237 Please set 'boot.initrd.systemd.users.root.shell' instead of 'boot.initrd.network.ssh.shell'
238 '';
239
240 boot.initrd.extraUtilsCommands = mkIf (!config.boot.initrd.systemd.enable) ''
241 copy_bin_and_libs ${package}/bin/sshd
242 copy_bin_and_libs ${package}/libexec/sshd-auth
243 copy_bin_and_libs ${package}/libexec/sshd-session
244 cp -pv ${pkgs.glibc.out}/lib/libnss_files.so.* $out/lib
245 '';
246
247 boot.initrd.extraUtilsCommandsTest = mkIf (!config.boot.initrd.systemd.enable) ''
248 # sshd requires a host key to check config, so we pass in the test's
249 tmpkey="$(mktemp initrd-ssh-testkey.XXXXXXXXXX)"
250 cp "${../../../tests/initrd-network-ssh/ssh_host_ed25519_key}" "$tmpkey"
251 # keys from Nix store are world-readable, which sshd doesn't like
252 chmod 600 "$tmpkey"
253 echo -n ${escapeShellArg sshdConfig} |
254 $out/bin/sshd -t -f /dev/stdin \
255 -h "$tmpkey"
256 rm "$tmpkey"
257 '';
258
259 boot.initrd.network.postCommands = mkIf (!config.boot.initrd.systemd.enable) ''
260 echo '${shell}' > /etc/shells
261 echo 'root:x:0:0:root:/root:${shell}' > /etc/passwd
262 echo 'sshd:x:1:1:sshd:/var/empty:/bin/nologin' >> /etc/passwd
263 echo 'passwd: files' > /etc/nsswitch.conf
264
265 mkdir -p /var/log /var/empty
266 touch /var/log/lastlog
267
268 mkdir -p /etc/ssh
269 echo -n ${escapeShellArg sshdConfig} > /etc/ssh/sshd_config
270
271 echo "export PATH=$PATH" >> /etc/profile
272 echo "export LD_LIBRARY_PATH=$LD_LIBRARY_PATH" >> /etc/profile
273
274 mkdir -p /root/.ssh
275 ${concatStrings (
276 map (key: ''
277 echo ${escapeShellArg key} >> /root/.ssh/authorized_keys
278 '') cfg.authorizedKeys
279 )}
280 ${concatStrings (
281 map (keyFile: ''
282 cat ${keyFile} >> /root/.ssh/authorized_keys
283 '') cfg.authorizedKeyFiles
284 )}
285
286 ${flip concatMapStrings cfg.hostKeys (path: ''
287 # keys from Nix store are world-readable, which sshd doesn't like
288 chmod 0600 "${initrdKeyPath path}"
289 '')}
290
291 /bin/sshd -e
292 '';
293
294 boot.initrd.postMountCommands = mkIf (!config.boot.initrd.systemd.enable) ''
295 # Stop sshd cleanly before stage 2.
296 #
297 # If you want to keep it around to debug post-mount SSH issues,
298 # run `touch /.keep_sshd` (either from an SSH session or in
299 # another initrd hook like preDeviceCommands).
300 if ! [ -e /.keep_sshd ]; then
301 pkill -x sshd
302 fi
303 '';
304
305 boot.initrd.secrets = listToAttrs (
306 map (path: nameValuePair (initrdKeyPath path) path) cfg.hostKeys
307 );
308
309 # Systemd initrd stuff
310 boot.initrd.systemd = mkIf config.boot.initrd.systemd.enable {
311 users.sshd = {
312 uid = 1;
313 group = "sshd";
314 };
315 groups.sshd = {
316 gid = 1;
317 };
318
319 users.root.shell = mkIf (
320 config.boot.initrd.network.ssh.shell != null
321 ) config.boot.initrd.network.ssh.shell;
322
323 contents = {
324 "/etc/ssh/sshd_config".text = sshdConfig;
325 "/etc/ssh/authorized_keys.d/root".text = concatStringsSep "\n" (
326 config.boot.initrd.network.ssh.authorizedKeys
327 ++ (map (file: lib.fileContents file) config.boot.initrd.network.ssh.authorizedKeyFiles)
328 );
329 };
330 storePaths = [
331 "${package}/bin/sshd"
332 "${package}/libexec/sshd-auth"
333 "${package}/libexec/sshd-session"
334 ];
335
336 services.sshd = {
337 description = "SSH Daemon";
338 wantedBy = [ "initrd.target" ];
339 after = [
340 "network.target"
341 "initrd-nixos-copy-secrets.service"
342 ];
343 before = [ "shutdown.target" ];
344 conflicts = [ "shutdown.target" ];
345
346 # Keys from Nix store are world-readable, which sshd doesn't
347 # like. If this were a real nix store and not the initrd, we
348 # neither would nor could do this
349 preStart = flip concatMapStrings cfg.hostKeys (path: ''
350 /bin/chmod 0600 "${initrdKeyPath path}"
351 '');
352 unitConfig.DefaultDependencies = false;
353 serviceConfig = {
354 ExecStart = "${package}/bin/sshd -D -f /etc/ssh/sshd_config";
355 Type = "simple";
356 KillMode = "process";
357 Restart = "on-failure";
358 };
359 };
360 };
361
362 };
363
364}