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