From 0698a1cf04392a24f740b4b5e16c5bd33642b6df Mon Sep 17 00:00:00 2001 From: Will Fancher Date: Wed, 3 Aug 2022 06:36:11 -0400 Subject: [PATCH] systemd-initrd: sshd --- nixos/modules/system/boot/initrd-ssh.nix | 64 +++++++++++++--- nixos/tests/all-tests.nix | 1 + nixos/tests/systemd-initrd-networkd-ssh.nix | 82 +++++++++++++++++++++ 3 files changed, 137 insertions(+), 10 deletions(-) create mode 100644 nixos/tests/systemd-initrd-networkd-ssh.nix diff --git a/nixos/modules/system/boot/initrd-ssh.nix b/nixos/modules/system/boot/initrd-ssh.nix index 125f75d66706..60c5ff62ffff 100644 --- a/nixos/modules/system/boot/initrd-ssh.nix +++ b/nixos/modules/system/boot/initrd-ssh.nix @@ -5,6 +5,10 @@ with lib; let cfg = config.boot.initrd.network.ssh; + shell = if cfg.shell == null then "/bin/ash" else cfg.shell; + inherit (config.programs.ssh) package; + + enabled = let initrd = config.boot.initrd; in (initrd.network.enable || initrd.systemd.network.enable) && cfg.enable; in @@ -33,8 +37,9 @@ in }; shell = mkOption { - type = types.str; - default = "/bin/ash"; + type = types.nullOr types.str; + default = null; + defaultText = ''"/bin/ash"''; description = lib.mdDoc '' Login shell of the remote user. Can be used to limit actions user can do. ''; @@ -119,9 +124,11 @@ in sshdCfg = config.services.openssh; sshdConfig = '' + UsePAM no Port ${toString cfg.port} PasswordAuthentication no + AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys2 /etc/ssh/authorized_keys.d/%u ChallengeResponseAuthentication no ${flip concatMapStrings cfg.hostKeys (path: '' @@ -142,7 +149,7 @@ in ${cfg.extraConfig} ''; - in mkIf (config.boot.initrd.network.enable && cfg.enable) { + in mkIf enabled { assertions = [ { assertion = cfg.authorizedKeys != []; @@ -157,14 +164,19 @@ in for instructions. ''; } + + { + assertion = config.boot.initrd.systemd.enable -> cfg.shell == null; + message = "systemd stage 1 does not support boot.initrd.network.ssh.shell"; + } ]; - boot.initrd.extraUtilsCommands = '' - copy_bin_and_libs ${pkgs.openssh}/bin/sshd + boot.initrd.extraUtilsCommands = mkIf (!config.boot.initrd.systemd.enable) '' + copy_bin_and_libs ${package}/bin/sshd cp -pv ${pkgs.glibc.out}/lib/libnss_files.so.* $out/lib ''; - boot.initrd.extraUtilsCommandsTest = '' + boot.initrd.extraUtilsCommandsTest = mkIf (!config.boot.initrd.systemd.enable) '' # sshd requires a host key to check config, so we pass in the test's tmpkey="$(mktemp initrd-ssh-testkey.XXXXXXXXXX)" cp "${../../../tests/initrd-network-ssh/ssh_host_ed25519_key}" "$tmpkey" @@ -176,9 +188,9 @@ in rm "$tmpkey" ''; - boot.initrd.network.postCommands = '' - echo '${cfg.shell}' > /etc/shells - echo 'root:x:0:0:root:/root:${cfg.shell}' > /etc/passwd + boot.initrd.network.postCommands = mkIf (!config.boot.initrd.systemd.enable) '' + echo '${shell}' > /etc/shells + echo 'root:x:0:0:root:/root:${shell}' > /etc/passwd echo 'sshd:x:1:1:sshd:/var/empty:/bin/nologin' >> /etc/passwd echo 'passwd: files' > /etc/nsswitch.conf @@ -204,7 +216,7 @@ in /bin/sshd -e ''; - boot.initrd.postMountCommands = '' + boot.initrd.postMountCommands = mkIf (!config.boot.initrd.systemd.enable) '' # Stop sshd cleanly before stage 2. # # If you want to keep it around to debug post-mount SSH issues, @@ -217,6 +229,38 @@ in boot.initrd.secrets = listToAttrs (map (path: nameValuePair (initrdKeyPath path) path) cfg.hostKeys); + + # Systemd initrd stuff + boot.initrd.systemd = mkIf config.boot.initrd.systemd.enable { + users.sshd = { uid = 1; group = "sshd"; }; + groups.sshd = { gid = 1; }; + + contents."/etc/ssh/authorized_keys.d/root".text = + concatStringsSep "\n" config.boot.initrd.network.ssh.authorizedKeys; + contents."/etc/ssh/sshd_config".text = sshdConfig; + storePaths = ["${package}/bin/sshd"]; + + services.sshd = { + description = "SSH Daemon"; + wantedBy = ["initrd.target"]; + after = ["network.target" "initrd-nixos-copy-secrets.service"]; + + # Keys from Nix store are world-readable, which sshd doesn't + # like. If this were a real nix store and not the initrd, we + # neither would nor could do this + preStart = flip concatMapStrings cfg.hostKeys (path: '' + /bin/chmod 0600 "${initrdKeyPath path}" + ''); + unitConfig.DefaultDependencies = false; + serviceConfig = { + ExecStart = "${package}/bin/sshd -D -f /etc/ssh/sshd_config"; + Type = "simple"; + KillMode = "process"; + Restart = "on-failure"; + }; + }; + }; + }; } diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 00a637b460f5..5771d1c3bc96 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -678,6 +678,7 @@ in { systemd-initrd-swraid = handleTest ./systemd-initrd-swraid.nix {}; systemd-initrd-vconsole = handleTest ./systemd-initrd-vconsole.nix {}; systemd-initrd-networkd = handleTest ./systemd-initrd-networkd.nix {}; + systemd-initrd-networkd-ssh = handleTest ./systemd-initrd-networkd-ssh.nix {}; systemd-journal = handleTest ./systemd-journal.nix {}; systemd-machinectl = handleTest ./systemd-machinectl.nix {}; systemd-networkd = handleTest ./systemd-networkd.nix {}; diff --git a/nixos/tests/systemd-initrd-networkd-ssh.nix b/nixos/tests/systemd-initrd-networkd-ssh.nix new file mode 100644 index 000000000000..943552613be9 --- /dev/null +++ b/nixos/tests/systemd-initrd-networkd-ssh.nix @@ -0,0 +1,82 @@ +import ./make-test-python.nix ({ lib, ... }: { + name = "systemd-initrd-network-ssh"; + meta.maintainers = [ lib.maintainers.elvishjerricco ]; + + nodes = with lib; { + server = { config, pkgs, ... }: { + environment.systemPackages = [pkgs.cryptsetup]; + boot.loader.systemd-boot.enable = true; + boot.loader.timeout = 0; + virtualisation = { + emptyDiskImages = [ 4096 ]; + useBootLoader = true; + useEFIBoot = true; + }; + + specialisation.encrypted-root.configuration = { + virtualisation.bootDevice = "/dev/mapper/root"; + boot.initrd.luks.devices = lib.mkVMOverride { + root.device = "/dev/vdc"; + }; + boot.initrd.systemd.enable = true; + boot.initrd.network = { + enable = true; + ssh = { + enable = true; + authorizedKeys = [ (readFile ./initrd-network-ssh/id_ed25519.pub) ]; + port = 22; + # Terrible hack so it works with useBootLoader + hostKeys = [ { outPath = "${./initrd-network-ssh/ssh_host_ed25519_key}"; } ]; + }; + }; + }; + }; + + client = { config, ... }: { + environment.etc = { + knownHosts = { + text = concatStrings [ + "server," + "${ + toString (head (splitString " " (toString + (elemAt (splitString "\n" config.networking.extraHosts) 2)))) + } " + "${readFile ./initrd-network-ssh/ssh_host_ed25519_key.pub}" + ]; + }; + sshKey = { + source = ./initrd-network-ssh/id_ed25519; + mode = "0600"; + }; + }; + }; + }; + + testScript = '' + start_all() + + def ssh_is_up(_) -> bool: + status, _ = client.execute("nc -z server 22") + return status == 0 + + server.wait_for_unit("multi-user.target") + server.succeed( + "echo somepass | cryptsetup luksFormat --type=luks2 /dev/vdc", + "bootctl set-default nixos-generation-1-specialisation-encrypted-root.conf", + "sync", + ) + server.shutdown() + server.start() + + client.wait_for_unit("network.target") + with client.nested("waiting for SSH server to come up"): + retry(ssh_is_up) + + client.succeed( + "echo somepass | ssh -i /etc/sshKey -o UserKnownHostsFile=/etc/knownHosts server 'systemd-tty-ask-password-agent' & exit" + ) + + server.wait_for_unit("multi-user.target") + server.succeed("mount | grep '/dev/mapper/root on /'") + ''; +})