at 24.11-pre 9.8 kB view raw
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}