diff options
author | Gregor Kleen <gkleen@yggdrasil.li> | 2023-01-13 10:55:43 +0100 |
---|---|---|
committer | Gregor Kleen <gkleen@yggdrasil.li> | 2023-01-13 10:55:43 +0100 |
commit | 4d7bf8c3e3dfce240c55b8fd863916dff0dc497c (patch) | |
tree | 450ffe476607ce4651113a665cfc1f90b4766955 | |
parent | 9cbd833e5d5c3c146bf0e2d2187f1dcf3a82d912 (diff) | |
download | nixos-4d7bf8c3e3dfce240c55b8fd863916dff0dc497c.tar nixos-4d7bf8c3e3dfce240c55b8fd863916dff0dc497c.tar.gz nixos-4d7bf8c3e3dfce240c55b8fd863916dff0dc497c.tar.bz2 nixos-4d7bf8c3e3dfce240c55b8fd863916dff0dc497c.tar.xz nixos-4d7bf8c3e3dfce240c55b8fd863916dff0dc497c.zip |
fixup initrd-ssh
-rw-r--r-- | system-profiles/initrd-ssh/default.nix | 46 | ||||
-rw-r--r-- | system-profiles/initrd-ssh/module.nix | 216 |
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 | |||
3 | with 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 | |||
3 | with lib; | ||
4 | |||
5 | let | ||
6 | |||
7 | cfg = config.boot.initrd.network.ssh; | ||
8 | |||
9 | in | ||
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 | } | ||