From 31e9ff1000324b0623ac0de5cfdebcf472a75774 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Mon, 9 Jun 2025 18:36:06 +0200 Subject: [PATCH 1/2] from-nix-seperated-phases: remove trailing whitespace --- tests/from-nixos-separated-phases.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/from-nixos-separated-phases.nix b/tests/from-nixos-separated-phases.nix index 27633702..e2a17d74 100644 --- a/tests/from-nixos-separated-phases.nix +++ b/tests/from-nixos-separated-phases.nix @@ -46,7 +46,7 @@ --debug \ --phases install \ --store-paths /etc/nixos-anywhere/disko /etc/nixos-anywhere/system-to-install \ - root@installed >&2 + root@installed >&2 """) ''; } From 48f06de82a55b9798750b590e2185be8cf4e959b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Mon, 9 Jun 2025 16:34:20 +0200 Subject: [PATCH 2/2] make installer with sudo-only access work and re-enable ssh-ng Now that our nixos installers should have a fixed ssh-ng implementation, it should be safe to re-enable it. On top we now test the use-case of an installer that doesn't need kexec but sudo explicitly. --- src/nixos-anywhere.sh | 123 ++++++++++++++++------- tests/flake-module.nix | 11 ++ tests/from-nixos-installer-with-sudo.nix | 50 +++++++++ tests/modules/system-to-install.nix | 74 ++++++++------ 4 files changed, 189 insertions(+), 69 deletions(-) create mode 100644 tests/from-nixos-installer-with-sudo.nix diff --git a/src/nixos-anywhere.sh b/src/nixos-anywhere.sh index e6746967..817a0dc1 100755 --- a/src/nixos-anywhere.sh +++ b/src/nixos-anywhere.sh @@ -64,7 +64,7 @@ mkdir -p "$tempDir" declare -A diskEncryptionKeys=() declare -A extraFilesOwnership=() -declare -a nixCopyOptions=() +declare -a nixCopyOptions=(--no-check-sigs) declare -a sshArgs=("-o" "IdentitiesOnly=yes" "-i" "$tempDir/nixos-anywhere" "-o" "UserKnownHostsFile=/dev/null" "-o" "StrictHostKeyChecking=no") showUsage() { @@ -420,19 +420,76 @@ runSsh() { ssh "$sshTtyParam" "${sshArgs[@]}" "$sshConnection" "$@" } +buildStoreUrl() { + local storeUrl="$1" + + # Add sshStoreSettings if present + if [[ -n ${sshStoreSettings} ]] && [[ $storeUrl == ssh-ng://* ]]; then + if [[ $storeUrl == *"?"* ]]; then + storeUrl="${storeUrl}&${sshStoreSettings}" + else + storeUrl="${storeUrl}?${sshStoreSettings}" + fi + fi + + # Add remote-program parameter when sudo is needed + if [[ -n ${maybeSudo} ]] && [[ $storeUrl == ssh-ng://* ]]; then + if [[ $storeUrl == *"?"* ]]; then + storeUrl="${storeUrl}&remote-program=${maybeSudo} nix-daemon" + else + storeUrl="${storeUrl}?remote-program=${maybeSudo} nix-daemon" + fi + fi + + echo "$storeUrl" +} + nixCopy() { - NIX_SSHOPTS="${sshArgs[*]}" nix copy \ - "${nixOptions[@]}" \ - "${nixCopyOptions[@]}" \ - "$@" + # Process arguments to add remote-program parameter when sudo is needed + local processedArgs=() + local i=1 + while [[ $i -le $# ]]; do + local arg="${!i}" + if [[ $arg == "--to" ]]; then + processedArgs+=("$arg") + ((i++)) + local storeUrl="${!i}" + storeUrl=$(buildStoreUrl "$storeUrl") + processedArgs+=("$storeUrl") + else + processedArgs+=("$arg") + fi + ((i++)) + done + + local nixCopyArgs=("${nixOptions[@]}" "${nixCopyOptions[@]}") + + NIX_SSHOPTS="${sshArgs[*]}" nix copy "${nixCopyArgs[@]}" "${processedArgs[@]}" } nixBuild() { + # Process arguments to add remote-program parameter when sudo is needed + local processedArgs=() + local i=1 + while [[ $i -le $# ]]; do + local arg="${!i}" + if [[ $arg == "--store" ]]; then + processedArgs+=("$arg") + ((i++)) + local storeUrl="${!i}" + storeUrl=$(buildStoreUrl "$storeUrl") + processedArgs+=("$storeUrl") + else + processedArgs+=("$arg") + fi + ((i++)) + done + NIX_SSHOPTS="${sshArgs[*]}" nix build \ --print-out-paths \ --no-link \ "${nixBuildFlags[@]}" \ "${nixOptions[@]}" \ - "$@" + "${processedArgs[@]}" } runVmTest() { @@ -688,6 +745,8 @@ TMPDIR=/root/kexec setsid --wait ${maybeSudo} /root/kexec/kexec/run --kexec-extr # After kexec we explicitly set the user to root@ sshConnection="root@${sshHost}" + # After kexec, we're running as root in the NixOS installer, so no need for sudo + maybeSudo="" # waiting for machine to become available again until runSsh -o ConnectTimeout=10 -- exit 0; do sleep 5; done @@ -697,55 +756,54 @@ runDisko() { local diskoScript=$1 for path in "${!diskEncryptionKeys[@]}"; do step "Uploading ${diskEncryptionKeys[$path]} to $path" - runSsh "umask 077; mkdir -p \"$(dirname "$path")\"; cat > $path" <"${diskEncryptionKeys[$path]}" + runSsh "${maybeSudo} sh -c $(printf '%q' "umask 077; mkdir -p $(dirname "$path"); cat > $path")" <"${diskEncryptionKeys[$path]}" done if [[ -n ${diskoScript} ]]; then - nixCopy --to "ssh://$sshConnection?$sshStoreSettings" "$diskoScript" + nixCopy --to "ssh-ng://$sshConnection" "$diskoScript" elif [[ ${buildOn} == "remote" ]]; then step Building disko script # We need to do a nix copy first because nix build doesn't have --no-check-sigs # Use ssh:// here to avoid https://github.com/NixOS/nix/issues/7359 - nixCopy --to "ssh://$sshConnection?$sshStoreSettings" "${flake}#${flakeAttr}.system.build.${diskoMode}Script" \ - --derivation --no-check-sigs + nixCopy --to "ssh-ng://$sshConnection" --derivation "${flake}#${flakeAttr}.system.build.${diskoMode}Script" # If we don't use ssh-ng here, we get `error: operation 'getFSAccessor' is not supported by store` diskoScript=$( nixBuild "${flake}#${flakeAttr}.system.build.${diskoAttr}" \ - --eval-store auto --store "ssh-ng://$sshConnection?ssh-key=$tempDir%2Fnixos-anywhere&$sshStoreSettings" + --eval-store auto --store "ssh-ng://$sshConnection?ssh-key=$tempDir%2Fnixos-anywhere" ) fi step Formatting hard drive with disko - runSsh "$diskoScript" + runSsh "${maybeSudo} $diskoScript" } nixosInstall() { local nixosSystem=$1 + local remoteStoreUrl="remote-store=local%3Froot=%2Fmnt" if [[ -n ${nixosSystem} ]]; then step Uploading the system closure - nixCopy --to "ssh://$sshConnection?remote-store=local%3Froot=%2Fmnt&$sshStoreSettings" "$nixosSystem" + nixCopy --to "ssh-ng://$sshConnection?${remoteStoreUrl}" "$nixosSystem" elif [[ ${buildOn} == "remote" ]]; then step Building the system closure # We need to do a nix copy first because nix build doesn't have --no-check-sigs # Use ssh:// here to avoid https://github.com/NixOS/nix/issues/7359 - nixCopy --to "ssh://$sshConnection?remote-store=local%3Froot=%2Fmnt&$sshStoreSettings" "${flake}#${flakeAttr}.system.build.toplevel" \ - --derivation --no-check-sigs + nixCopy --to "ssh-ng://$sshConnection?${remoteStoreUrl}" --derivation "${flake}#${flakeAttr}.system.build.toplevel" # If we don't use ssh-ng here, we get `error: operation 'getFSAccessor' is not supported by store` nixosSystem=$( nixBuild "${flake}#${flakeAttr}.system.build.toplevel" \ - --eval-store auto --store "ssh-ng://$sshConnection?ssh-key=$tempDir%2Fnixos-anywhere&remote-store=local%3Froot=%2Fmnt&$sshStoreSettings" + --eval-store auto --store "ssh-ng://$sshConnection?ssh-key=$tempDir%2Fnixos-anywhere&${remoteStoreUrl}" ) fi if [[ -n ${extraFiles} ]]; then step Copying extra files - tar -C "$extraFiles" -cpf- . | runSsh "tar -C /mnt -xf- --no-same-owner" + tar -C "$extraFiles" -cpf- . | runSsh "${maybeSudo} tar -C /mnt -xf- --no-same-owner" - runSsh "chmod 755 /mnt" # tar also changes permissions of /mnt + runSsh "${maybeSudo} chmod 755 /mnt" # tar also changes permissions of /mnt fi if [[ ${#extraFilesOwnership[@]} -gt 0 ]]; then # shellcheck disable=SC2016 - printf "%s\n" "${!extraFilesOwnership[@]}" "${extraFilesOwnership[@]}" | pr -2t | runSsh 'while read file ownership; do chown -R "$ownership" "/mnt/$file"; done' + printf "%s\n" "${!extraFilesOwnership[@]}" "${extraFilesOwnership[@]}" | pr -2t | runSsh 'while read file ownership; do '"${maybeSudo}"' chown -R "$ownership" "/mnt/$file"; done' fi step Installing NixOS @@ -756,27 +814,27 @@ export PATH="\$PATH:/run/current-system/sw/bin" if [ ! -d "/mnt/tmp" ]; then # needed for installation if initrd-secrets are used - mkdir -p /mnt/tmp - chmod 777 /mnt/tmp + ${maybeSudo} mkdir -p /mnt/tmp + ${maybeSudo} chmod 777 /mnt/tmp fi if [ ${copyHostKeys-n} = "y" ]; then # NB we copy host keys that are in turn copied by kexec installer. - mkdir -m 755 -p /mnt/etc/ssh + ${maybeSudo} mkdir -m 755 -p /mnt/etc/ssh for p in /etc/ssh/ssh_host_*; do # Skip if the source file does not exist (i.e. glob did not match any files) # or the destination already exists (e.g. copied with --extra-files). if [ ! -e "\$p" ] || [ -e "/mnt/\$p" ]; then continue fi - cp -a "\$p" "/mnt/\$p" + ${maybeSudo} cp -a "\$p" "/mnt/\$p" done fi # https://stackoverflow.com/a/13864829 if [ ! -z ${NIXOS_NO_CHECK+0} ]; then export NIXOS_NO_CHECK fi -nixos-install --no-root-passwd --no-channel-copy --system "$nixosSystem" +${maybeSudo} nixos-install --no-root-passwd --no-channel-copy --system "$nixosSystem" SSH } @@ -786,11 +844,11 @@ nixosReboot() { runSsh sh </dev/null && [ "\$(zpool list)" != "no pools available" ]; then # we always want to export the zfs pools so people can boot from it without force import - umount -Rv /mnt/ - swapoff -a - zpool export -a || true + ${maybeSudo} umount -Rv /mnt/ + ${maybeSudo} swapoff -a + ${maybeSudo} zpool export -a || true fi - nohup sh -c 'sleep 6 && reboot' >/dev/null & + ${maybeSudo} nohup sh -c 'sleep 6 && reboot' >/dev/null & SSH step Waiting for the machine to become unreachable due to reboot @@ -840,7 +898,6 @@ main() { fi sshSettings=$(ssh "${sshArgs[@]}" -G "${sshConnection}") - sshUser=$(echo "$sshSettings" | awk '/^user / { print $2 }') sshHost=$(echo "$sshSettings" | awk '/^hostname / { print $2 }') uploadSshKey @@ -898,14 +955,6 @@ main() { fi fi - # Installation will fail if non-root user is used for installer. - # Switch to root user by copying authorized_keys. - if [[ ${isInstaller} == "y" ]] && [[ ${sshUser} != "root" ]]; then - # Allow copy to fail if authorized_keys does not exist, like if using /etc/ssh/authorized_keys.d/ - runSsh "${maybeSudo} mkdir -p /root/.ssh; ${maybeSudo} cp ~/.ssh/authorized_keys /root/.ssh || true" - sshConnection="root@${sshHost}" - fi - if [[ ${phases[disko]} == 1 ]]; then runDisko "$diskoScript" fi diff --git a/tests/flake-module.nix b/tests/flake-module.nix index dde92cf7..ea76da6c 100644 --- a/tests/flake-module.nix +++ b/tests/flake-module.nix @@ -7,6 +7,13 @@ ./modules/system-to-install.nix inputs.disko.nixosModules.disko ]; + system-to-install-vdb = pkgs.nixos [ + ./modules/system-to-install.nix + inputs.disko.nixosModules.disko + { + nixos-anywhere.diskDevice = "/dev/vdb"; + } + ]; testInputsUnstable = { inherit pkgs; inherit (inputs.disko.nixosModules) disko; @@ -17,6 +24,9 @@ testInputsStable = testInputsUnstable // { kexec-installer = "${inputs'.nixos-images.packages.kexec-installer-nixos-stable-noninteractive}/nixos-kexec-installer-noninteractive-${system}.tar.gz"; }; + testInputsInstallerSudo = testInputsUnstable // { + system-to-install = system-to-install-vdb; + }; linuxTestInputs = testInputsUnstable // { nix-vm-test = inputs.nix-vm-test; }; @@ -26,6 +36,7 @@ from-nixos-stable = import ./from-nixos.nix testInputsStable; from-nixos-with-sudo = import ./from-nixos-with-sudo.nix testInputsUnstable; from-nixos-with-sudo-stable = import ./from-nixos-with-sudo.nix testInputsStable; + from-nixos-installer-with-sudo = import ./from-nixos-installer-with-sudo.nix testInputsInstallerSudo; from-nixos-with-generated-config = import ./from-nixos-generate-config.nix testInputsUnstable; from-nixos-build-on-remote = import ./from-nixos-build-on-remote.nix testInputsUnstable; from-nixos-separated-phases = import ./from-nixos-separated-phases.nix testInputsUnstable; diff --git a/tests/from-nixos-installer-with-sudo.nix b/tests/from-nixos-installer-with-sudo.nix new file mode 100644 index 00000000..66da14e9 --- /dev/null +++ b/tests/from-nixos-installer-with-sudo.nix @@ -0,0 +1,50 @@ +(import ./lib/test-base.nix) { + name = "from-nixos-installer-with-sudo"; + nodes = { + installer = ./modules/installer.nix; + installed = { modulesPath, ... }: { + imports = [ + (modulesPath + "/installer/cd-dvd/installation-cd-base.nix") + ]; + + services.openssh.enable = true; + virtualisation.memorySize = 1500; + virtualisation.emptyDiskImages = [ 1024 ]; + + users.users.nixos = { + isNormalUser = true; + openssh.authorizedKeys.keyFiles = [ ./modules/ssh-keys/ssh.pub ]; + extraGroups = [ "wheel" ]; + }; + security.sudo.enable = true; + security.sudo.wheelNeedsPassword = false; + + # Configure nix trusted users for remote builds with sudo + nix.settings.trusted-users = [ "root" "nixos" ]; + }; + }; + testScript = '' + start_all() + installer.succeed("echo super-secret > /tmp/disk-1.key") + installer.succeed("mkdir -p /tmp/extra-files/var/lib/secrets") + installer.succeed("echo test-value > /tmp/extra-files/var/lib/secrets/test") + + output = installer.succeed(""" + nixos-anywhere \ + -i /root/.ssh/install_key \ + --debug \ + --phases disko,install \ + --disk-encryption-keys /tmp/disk-1.key /tmp/disk-1.key \ + --extra-files /tmp/extra-files \ + --store-paths /etc/nixos-anywhere/disko /etc/nixos-anywhere/system-to-install \ + nixos@installed >&2 + echo "disk-1.key: '$(ssh -i /root/.ssh/install_key -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no \ + nixos@installed sudo cat /tmp/disk-1.key)'" + echo "extra-file: '$(ssh -i /root/.ssh/install_key -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no \ + nixos@installed sudo cat /mnt/var/lib/secrets/test)'" + """) + + assert "disk-1.key: 'super-secret'" in output, f"output does not contain expected values: {output}" + assert "extra-file: 'test-value'" in output, f"output does not contain expected values: {output}" + ''; +} diff --git a/tests/modules/system-to-install.nix b/tests/modules/system-to-install.nix index ea5f2a37..def5968e 100644 --- a/tests/modules/system-to-install.nix +++ b/tests/modules/system-to-install.nix @@ -1,41 +1,51 @@ -{ modulesPath, self, lib, ... }: { +{ modulesPath, lib, config, ... }: +{ + options.nixos-anywhere.diskDevice = lib.mkOption { + type = lib.types.str; + default = "/dev/vda"; + description = "The disk device to use for installation"; + }; + imports = [ (modulesPath + "/testing/test-instrumentation.nix") (modulesPath + "/profiles/qemu-guest.nix") (modulesPath + "/profiles/minimal.nix") ]; - networking.hostName = lib.mkDefault "nixos-anywhere"; - documentation.enable = false; - hardware.enableAllFirmware = false; - networking.hostId = "8425e349"; # from profiles/base.nix, needed for zfs - boot.zfs.devNodes = "/dev/disk/by-uuid"; # needed because /dev/disk/by-id is empty in qemu-vms - disko.devices = { - disk = { - vda = { - device = "/dev/vda"; - type = "disk"; - content = { - type = "gpt"; - partitions = { - boot = { - size = "1M"; - type = "EF02"; - }; - ESP = { - size = "100M"; - type = "EF00"; - content = { - type = "filesystem"; - format = "vfat"; - mountpoint = "/boot"; + + config = { + networking.hostName = lib.mkDefault "nixos-anywhere"; + documentation.enable = false; + hardware.enableAllFirmware = false; + networking.hostId = "8425e349"; # from profiles/base.nix, needed for zfs + boot.zfs.devNodes = "/dev/disk/by-uuid"; # needed because /dev/disk/by-id is empty in qemu-vms + disko.devices = { + disk = { + main = { + device = config.nixos-anywhere.diskDevice; + type = "disk"; + content = { + type = "gpt"; + partitions = { + boot = { + size = "1M"; + type = "EF02"; }; - }; - root = { - size = "100%"; - content = { - type = "filesystem"; - format = "ext4"; - mountpoint = "/"; + ESP = { + size = "100M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + }; + }; + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; }; }; };