at 23.11-pre 8.7 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 = lib.mdDoc '' 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 = lib.mdDoc '' 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 = lib.mdDoc '' 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 = lib.mdDoc '' 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 = lib.mdDoc '' 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 = lib.mdDoc '' 95 Authorized keys for the root user on initrd. 96 ''; 97 }; 98 99 extraConfig = mkOption { 100 type = types.lines; 101 default = ""; 102 description = lib.mdDoc "Verbatim contents of {file}`sshd_config`."; 103 }; 104 }; 105 106 imports = 107 map (opt: mkRemovedOptionModule ([ "boot" "initrd" "network" "ssh" ] ++ [ opt ]) '' 108 The initrd SSH functionality now uses OpenSSH rather than Dropbear. 109 110 If you want to keep your existing initrd SSH host keys, convert them with 111 $ dropbearconvert dropbear openssh dropbear_host_$type_key ssh_host_$type_key 112 and then set options.boot.initrd.network.ssh.hostKeys. 113 '') [ "hostRSAKey" "hostDSSKey" "hostECDSAKey" ]; 114 115 config = let 116 # Nix complains if you include a store hash in initrd path names, so 117 # as an awful hack we drop the first character of the hash. 118 initrdKeyPath = path: if isString path 119 then path 120 else let name = builtins.baseNameOf path; in 121 builtins.unsafeDiscardStringContext ("/etc/ssh/" + 122 substring 1 (stringLength name) name); 123 124 sshdCfg = config.services.openssh; 125 126 sshdConfig = '' 127 UsePAM no 128 Port ${toString cfg.port} 129 130 PasswordAuthentication no 131 AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys2 /etc/ssh/authorized_keys.d/%u 132 ChallengeResponseAuthentication no 133 134 ${flip concatMapStrings cfg.hostKeys (path: '' 135 HostKey ${initrdKeyPath path} 136 '')} 137 138 KexAlgorithms ${concatStringsSep "," sshdCfg.settings.KexAlgorithms} 139 Ciphers ${concatStringsSep "," sshdCfg.settings.Ciphers} 140 MACs ${concatStringsSep "," sshdCfg.settings.Macs} 141 142 LogLevel ${sshdCfg.settings.LogLevel} 143 144 ${if sshdCfg.settings.UseDns then '' 145 UseDNS yes 146 '' else '' 147 UseDNS no 148 ''} 149 150 ${cfg.extraConfig} 151 ''; 152 in mkIf enabled { 153 assertions = [ 154 { 155 assertion = cfg.authorizedKeys != []; 156 message = "You should specify at least one authorized key for initrd SSH"; 157 } 158 159 { 160 assertion = (cfg.hostKeys != []) || cfg.ignoreEmptyHostKeys; 161 message = '' 162 You must now pre-generate the host keys for initrd SSH. 163 See the boot.initrd.network.ssh.hostKeys documentation 164 for instructions. 165 ''; 166 } 167 168 { 169 assertion = config.boot.initrd.systemd.enable -> cfg.shell == null; 170 message = "systemd stage 1 does not support boot.initrd.network.ssh.shell"; 171 } 172 ]; 173 174 boot.initrd.extraUtilsCommands = mkIf (!config.boot.initrd.systemd.enable) '' 175 copy_bin_and_libs ${package}/bin/sshd 176 cp -pv ${pkgs.glibc.out}/lib/libnss_files.so.* $out/lib 177 ''; 178 179 boot.initrd.extraUtilsCommandsTest = mkIf (!config.boot.initrd.systemd.enable) '' 180 # sshd requires a host key to check config, so we pass in the test's 181 tmpkey="$(mktemp initrd-ssh-testkey.XXXXXXXXXX)" 182 cp "${../../../tests/initrd-network-ssh/ssh_host_ed25519_key}" "$tmpkey" 183 # keys from Nix store are world-readable, which sshd doesn't like 184 chmod 600 "$tmpkey" 185 echo -n ${escapeShellArg sshdConfig} | 186 $out/bin/sshd -t -f /dev/stdin \ 187 -h "$tmpkey" 188 rm "$tmpkey" 189 ''; 190 191 boot.initrd.network.postCommands = mkIf (!config.boot.initrd.systemd.enable) '' 192 echo '${shell}' > /etc/shells 193 echo 'root:x:0:0:root:/root:${shell}' > /etc/passwd 194 echo 'sshd:x:1:1:sshd:/var/empty:/bin/nologin' >> /etc/passwd 195 echo 'passwd: files' > /etc/nsswitch.conf 196 197 mkdir -p /var/log /var/empty 198 touch /var/log/lastlog 199 200 mkdir -p /etc/ssh 201 echo -n ${escapeShellArg sshdConfig} > /etc/ssh/sshd_config 202 203 echo "export PATH=$PATH" >> /etc/profile 204 echo "export LD_LIBRARY_PATH=$LD_LIBRARY_PATH" >> /etc/profile 205 206 mkdir -p /root/.ssh 207 ${concatStrings (map (key: '' 208 echo ${escapeShellArg key} >> /root/.ssh/authorized_keys 209 '') cfg.authorizedKeys)} 210 211 ${flip concatMapStrings cfg.hostKeys (path: '' 212 # keys from Nix store are world-readable, which sshd doesn't like 213 chmod 0600 "${initrdKeyPath path}" 214 '')} 215 216 /bin/sshd -e 217 ''; 218 219 boot.initrd.postMountCommands = mkIf (!config.boot.initrd.systemd.enable) '' 220 # Stop sshd cleanly before stage 2. 221 # 222 # If you want to keep it around to debug post-mount SSH issues, 223 # run `touch /.keep_sshd` (either from an SSH session or in 224 # another initrd hook like preDeviceCommands). 225 if ! [ -e /.keep_sshd ]; then 226 pkill -x sshd 227 fi 228 ''; 229 230 boot.initrd.secrets = listToAttrs 231 (map (path: nameValuePair (initrdKeyPath path) path) cfg.hostKeys); 232 233 # Systemd initrd stuff 234 boot.initrd.systemd = mkIf config.boot.initrd.systemd.enable { 235 users.sshd = { uid = 1; group = "sshd"; }; 236 groups.sshd = { gid = 1; }; 237 238 contents."/etc/ssh/authorized_keys.d/root".text = 239 concatStringsSep "\n" config.boot.initrd.network.ssh.authorizedKeys; 240 contents."/etc/ssh/sshd_config".text = sshdConfig; 241 storePaths = ["${package}/bin/sshd"]; 242 243 services.sshd = { 244 description = "SSH Daemon"; 245 wantedBy = ["initrd.target"]; 246 after = ["network.target" "initrd-nixos-copy-secrets.service"]; 247 248 # Keys from Nix store are world-readable, which sshd doesn't 249 # like. If this were a real nix store and not the initrd, we 250 # neither would nor could do this 251 preStart = flip concatMapStrings cfg.hostKeys (path: '' 252 /bin/chmod 0600 "${initrdKeyPath path}" 253 ''); 254 unitConfig.DefaultDependencies = false; 255 serviceConfig = { 256 ExecStart = "${package}/bin/sshd -D -f /etc/ssh/sshd_config"; 257 Type = "simple"; 258 KillMode = "process"; 259 Restart = "on-failure"; 260 }; 261 }; 262 }; 263 264 }; 265 266}