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