1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6
7 cfg = config.boot.initrd.network.ssh;
8
9in
10
11{
12
13 options.boot.initrd.network.ssh = {
14 enable = mkOption {
15 type = types.bool;
16 default = false;
17 description = lib.mdDoc ''
18 Start SSH service during initrd boot. It can be used to debug failing
19 boot on a remote server, enter pasphrase for an encrypted partition etc.
20 Service is killed when stage-1 boot is finished.
21
22 The sshd configuration is largely inherited from
23 {option}`services.openssh`.
24 '';
25 };
26
27 port = mkOption {
28 type = types.port;
29 default = 22;
30 description = lib.mdDoc ''
31 Port on which SSH initrd service should listen.
32 '';
33 };
34
35 shell = mkOption {
36 type = types.str;
37 default = "/bin/ash";
38 description = lib.mdDoc ''
39 Login shell of the remote user. Can be used to limit actions user can do.
40 '';
41 };
42
43 hostKeys = mkOption {
44 type = types.listOf (types.either types.str types.path);
45 default = [];
46 example = [
47 "/etc/secrets/initrd/ssh_host_rsa_key"
48 "/etc/secrets/initrd/ssh_host_ed25519_key"
49 ];
50 description = lib.mdDoc ''
51 Specify SSH host keys to import into the initrd.
52
53 To generate keys, use
54 {manpage}`ssh-keygen(1)`
55 as root:
56
57 ```
58 ssh-keygen -t rsa -N "" -f /etc/secrets/initrd/ssh_host_rsa_key
59 ssh-keygen -t ed25519 -N "" -f /etc/secrets/initrd/ssh_host_ed25519_key
60 ```
61
62 ::: {.warning}
63 Unless your bootloader supports initrd secrets, these keys
64 are stored insecurely in the global Nix store. Do NOT use
65 your regular SSH host private keys for this purpose or
66 you'll expose them to regular users!
67
68 Additionally, even if your initrd supports secrets, if
69 you're using initrd SSH to unlock an encrypted disk then
70 using your regular host keys exposes the private keys on
71 your unencrypted boot partition.
72 :::
73 '';
74 };
75
76 authorizedKeys = mkOption {
77 type = types.listOf types.str;
78 default = config.users.users.root.openssh.authorizedKeys.keys;
79 defaultText = literalExpression "config.users.users.root.openssh.authorizedKeys.keys";
80 description = lib.mdDoc ''
81 Authorized keys for the root user on initrd.
82 '';
83 };
84
85 extraConfig = mkOption {
86 type = types.lines;
87 default = "";
88 description = lib.mdDoc "Verbatim contents of {file}`sshd_config`.";
89 };
90 };
91
92 imports =
93 map (opt: mkRemovedOptionModule ([ "boot" "initrd" "network" "ssh" ] ++ [ opt ]) ''
94 The initrd SSH functionality now uses OpenSSH rather than Dropbear.
95
96 If you want to keep your existing initrd SSH host keys, convert them with
97 $ dropbearconvert dropbear openssh dropbear_host_$type_key ssh_host_$type_key
98 and then set options.boot.initrd.network.ssh.hostKeys.
99 '') [ "hostRSAKey" "hostDSSKey" "hostECDSAKey" ];
100
101 config = let
102 # Nix complains if you include a store hash in initrd path names, so
103 # as an awful hack we drop the first character of the hash.
104 initrdKeyPath = path: if isString path
105 then path
106 else let name = builtins.baseNameOf path; in
107 builtins.unsafeDiscardStringContext ("/etc/ssh/" +
108 substring 1 (stringLength name) name);
109
110 sshdCfg = config.services.openssh;
111
112 sshdConfig = ''
113 Port ${toString cfg.port}
114
115 PasswordAuthentication no
116 ChallengeResponseAuthentication no
117
118 ${flip concatMapStrings cfg.hostKeys (path: ''
119 HostKey ${initrdKeyPath path}
120 '')}
121
122 KexAlgorithms ${concatStringsSep "," sshdCfg.kexAlgorithms}
123 Ciphers ${concatStringsSep "," sshdCfg.ciphers}
124 MACs ${concatStringsSep "," sshdCfg.macs}
125
126 LogLevel ${sshdCfg.logLevel}
127
128 ${if sshdCfg.useDns then ''
129 UseDNS yes
130 '' else ''
131 UseDNS no
132 ''}
133
134 ${cfg.extraConfig}
135 '';
136 in mkIf (config.boot.initrd.network.enable && cfg.enable) {
137 assertions = [
138 {
139 assertion = cfg.authorizedKeys != [];
140 message = "You should specify at least one authorized key for initrd SSH";
141 }
142
143 {
144 assertion = cfg.hostKeys != [];
145 message = ''
146 You must now pre-generate the host keys for initrd SSH.
147 See the boot.initrd.network.ssh.hostKeys documentation
148 for instructions.
149 '';
150 }
151 ];
152
153 boot.initrd.extraUtilsCommands = ''
154 copy_bin_and_libs ${pkgs.openssh}/bin/sshd
155 cp -pv ${pkgs.glibc.out}/lib/libnss_files.so.* $out/lib
156 '';
157
158 boot.initrd.extraUtilsCommandsTest = ''
159 # sshd requires a host key to check config, so we pass in the test's
160 tmpkey="$(mktemp initrd-ssh-testkey.XXXXXXXXXX)"
161 cp "${../../../tests/initrd-network-ssh/ssh_host_ed25519_key}" "$tmpkey"
162 # keys from Nix store are world-readable, which sshd doesn't like
163 chmod 600 "$tmpkey"
164 echo -n ${escapeShellArg sshdConfig} |
165 $out/bin/sshd -t -f /dev/stdin \
166 -h "$tmpkey"
167 rm "$tmpkey"
168 '';
169
170 boot.initrd.network.postCommands = ''
171 echo '${cfg.shell}' > /etc/shells
172 echo 'root:x:0:0:root:/root:${cfg.shell}' > /etc/passwd
173 echo 'sshd:x:1:1:sshd:/var/empty:/bin/nologin' >> /etc/passwd
174 echo 'passwd: files' > /etc/nsswitch.conf
175
176 mkdir -p /var/log /var/empty
177 touch /var/log/lastlog
178
179 mkdir -p /etc/ssh
180 echo -n ${escapeShellArg sshdConfig} > /etc/ssh/sshd_config
181
182 echo "export PATH=$PATH" >> /etc/profile
183 echo "export LD_LIBRARY_PATH=$LD_LIBRARY_PATH" >> /etc/profile
184
185 mkdir -p /root/.ssh
186 ${concatStrings (map (key: ''
187 echo ${escapeShellArg key} >> /root/.ssh/authorized_keys
188 '') cfg.authorizedKeys)}
189
190 ${flip concatMapStrings cfg.hostKeys (path: ''
191 # keys from Nix store are world-readable, which sshd doesn't like
192 chmod 0600 "${initrdKeyPath path}"
193 '')}
194
195 /bin/sshd -e
196 '';
197
198 boot.initrd.postMountCommands = ''
199 # Stop sshd cleanly before stage 2.
200 #
201 # If you want to keep it around to debug post-mount SSH issues,
202 # run `touch /.keep_sshd` (either from an SSH session or in
203 # another initrd hook like preDeviceCommands).
204 if ! [ -e /.keep_sshd ]; then
205 pkill -x sshd
206 fi
207 '';
208
209 boot.initrd.secrets = listToAttrs
210 (map (path: nameValuePair (initrdKeyPath path) path) cfg.hostKeys);
211 };
212
213}