Skip to content

Commit 1434cb9

Browse files
committed
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.
1 parent 31e9ff1 commit 1434cb9

File tree

4 files changed

+176
-67
lines changed

4 files changed

+176
-67
lines changed

src/nixos-anywhere.sh

Lines changed: 73 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ mkdir -p "$tempDir"
6464

6565
declare -A diskEncryptionKeys=()
6666
declare -A extraFilesOwnership=()
67-
declare -a nixCopyOptions=()
67+
declare -a nixCopyOptions=(--no-check-sigs)
6868
declare -a sshArgs=("-o" "IdentitiesOnly=yes" "-i" "$tempDir/nixos-anywhere" "-o" "UserKnownHostsFile=/dev/null" "-o" "StrictHostKeyChecking=no")
6969

7070
showUsage() {
@@ -420,19 +420,64 @@ runSsh() {
420420
ssh "$sshTtyParam" "${sshArgs[@]}" "$sshConnection" "$@"
421421
}
422422

423+
addRemoteProgram() {
424+
local storeUrl="$1"
425+
# Add remote-program parameter when sudo is needed
426+
if [[ -n ${maybeSudo} ]] && [[ $storeUrl == ssh-ng://* ]]; then
427+
if [[ $storeUrl == *"?"* ]]; then
428+
echo "${storeUrl}&remote-program=${maybeSudo} nix-daemon"
429+
else
430+
echo "${storeUrl}?remote-program=${maybeSudo} nix-daemon"
431+
fi
432+
else
433+
echo "$storeUrl"
434+
fi
435+
}
436+
423437
nixCopy() {
424-
NIX_SSHOPTS="${sshArgs[*]}" nix copy \
425-
"${nixOptions[@]}" \
426-
"${nixCopyOptions[@]}" \
427-
"$@"
438+
# Process arguments to add remote-program parameter when sudo is needed
439+
local processedArgs=()
440+
local i=0
441+
while [[ $i -lt $# ]]; do
442+
if [[ ${!i} == "--to" ]]; then
443+
processedArgs+=("${!i}")
444+
((i++))
445+
local storeUrl="${!i}"
446+
storeUrl=$(addRemoteProgram "$storeUrl")
447+
processedArgs+=("$storeUrl")
448+
else
449+
processedArgs+=("${!i}")
450+
fi
451+
((i++))
452+
done
453+
454+
local nixCopyArgs=("${nixOptions[@]}" "${nixCopyOptions[@]}")
455+
456+
NIX_SSHOPTS="${sshArgs[*]}" nix copy "${nixCopyArgs[@]}" "${processedArgs[@]}"
428457
}
429458
nixBuild() {
459+
# Process arguments to add remote-program parameter when sudo is needed
460+
local processedArgs=()
461+
local i=0
462+
while [[ $i -lt $# ]]; do
463+
if [[ ${!i} == "--store" ]]; then
464+
processedArgs+=("${!i}")
465+
((i++))
466+
local storeUrl="${!i}"
467+
storeUrl=$(addRemoteProgram "$storeUrl")
468+
processedArgs+=("$storeUrl")
469+
else
470+
processedArgs+=("${!i}")
471+
fi
472+
((i++))
473+
done
474+
430475
NIX_SSHOPTS="${sshArgs[*]}" nix build \
431476
--print-out-paths \
432477
--no-link \
433478
"${nixBuildFlags[@]}" \
434479
"${nixOptions[@]}" \
435-
"$@"
480+
"${processedArgs[@]}"
436481
}
437482

438483
runVmTest() {
@@ -688,6 +733,8 @@ TMPDIR=/root/kexec setsid --wait ${maybeSudo} /root/kexec/kexec/run --kexec-extr
688733
689734
# After kexec we explicitly set the user to root@
690735
sshConnection="root@${sshHost}"
736+
# After kexec, we're running as root in the NixOS installer, so no need for sudo
737+
maybeSudo=""
691738
692739
# waiting for machine to become available again
693740
until runSsh -o ConnectTimeout=10 -- exit 0; do sleep 5; done
@@ -697,16 +744,15 @@ runDisko() {
697744
local diskoScript=$1
698745
for path in "${!diskEncryptionKeys[@]}"; do
699746
step "Uploading ${diskEncryptionKeys[$path]} to $path"
700-
runSsh "umask 077; mkdir -p \"$(dirname "$path")\"; cat > $path" <"${diskEncryptionKeys[$path]}"
747+
runSsh "${maybeSudo} sh -c $(printf '%q' "umask 077; mkdir -p $(dirname "$path"); cat > $path")" <"${diskEncryptionKeys[$path]}"
701748
done
702749
if [[ -n ${diskoScript} ]]; then
703-
nixCopy --to "ssh://$sshConnection?$sshStoreSettings" "$diskoScript"
750+
nixCopy --to "ssh-ng://$sshConnection?$sshStoreSettings" "$diskoScript"
704751
elif [[ ${buildOn} == "remote" ]]; then
705752
step Building disko script
706753
# We need to do a nix copy first because nix build doesn't have --no-check-sigs
707754
# Use ssh:// here to avoid https://github.com/NixOS/nix/issues/7359
708-
nixCopy --to "ssh://$sshConnection?$sshStoreSettings" "${flake}#${flakeAttr}.system.build.${diskoMode}Script" \
709-
--derivation --no-check-sigs
755+
nixCopy --to "ssh-ng://$sshConnection?$sshStoreSettings" --derivation "${flake}#${flakeAttr}.system.build.${diskoMode}Script"
710756
# If we don't use ssh-ng here, we get `error: operation 'getFSAccessor' is not supported by store`
711757
diskoScript=$(
712758
nixBuild "${flake}#${flakeAttr}.system.build.${diskoAttr}" \
@@ -715,37 +761,37 @@ runDisko() {
715761
fi
716762
717763
step Formatting hard drive with disko
718-
runSsh "$diskoScript"
764+
runSsh "${maybeSudo} $diskoScript"
719765
}
720766
721767
nixosInstall() {
722768
local nixosSystem=$1
769+
local remoteStoreUrl="remote-store=local%3Froot=%2Fmnt"
723770
if [[ -n ${nixosSystem} ]]; then
724771
step Uploading the system closure
725-
nixCopy --to "ssh://$sshConnection?remote-store=local%3Froot=%2Fmnt&$sshStoreSettings" "$nixosSystem"
772+
nixCopy --to "ssh-ng://$sshConnection?${remoteStoreUrl}&$sshStoreSettings" "$nixosSystem"
726773
elif [[ ${buildOn} == "remote" ]]; then
727774
step Building the system closure
728775
# We need to do a nix copy first because nix build doesn't have --no-check-sigs
729776
# Use ssh:// here to avoid https://github.com/NixOS/nix/issues/7359
730-
nixCopy --to "ssh://$sshConnection?remote-store=local%3Froot=%2Fmnt&$sshStoreSettings" "${flake}#${flakeAttr}.system.build.toplevel" \
731-
--derivation --no-check-sigs
777+
nixCopy --to "ssh-ng://$sshConnection?${remoteStoreUrl}&$sshStoreSettings" --derivation "${flake}#${flakeAttr}.system.build.toplevel"
732778
# If we don't use ssh-ng here, we get `error: operation 'getFSAccessor' is not supported by store`
733779
nixosSystem=$(
734780
nixBuild "${flake}#${flakeAttr}.system.build.toplevel" \
735-
--eval-store auto --store "ssh-ng://$sshConnection?ssh-key=$tempDir%2Fnixos-anywhere&remote-store=local%3Froot=%2Fmnt&$sshStoreSettings"
781+
--eval-store auto --store "ssh-ng://$sshConnection?ssh-key=$tempDir%2Fnixos-anywhere&${remoteStoreUrl}&$sshStoreSettings"
736782
)
737783
fi
738784
739785
if [[ -n ${extraFiles} ]]; then
740786
step Copying extra files
741-
tar -C "$extraFiles" -cpf- . | runSsh "tar -C /mnt -xf- --no-same-owner"
787+
tar -C "$extraFiles" -cpf- . | runSsh "${maybeSudo} tar -C /mnt -xf- --no-same-owner"
742788
743-
runSsh "chmod 755 /mnt" # tar also changes permissions of /mnt
789+
runSsh "${maybeSudo} chmod 755 /mnt" # tar also changes permissions of /mnt
744790
fi
745791
746792
if [[ ${#extraFilesOwnership[@]} -gt 0 ]]; then
747793
# shellcheck disable=SC2016
748-
printf "%s\n" "${!extraFilesOwnership[@]}" "${extraFilesOwnership[@]}" | pr -2t | runSsh 'while read file ownership; do chown -R "$ownership" "/mnt/$file"; done'
794+
printf "%s\n" "${!extraFilesOwnership[@]}" "${extraFilesOwnership[@]}" | pr -2t | runSsh 'while read file ownership; do '"${maybeSudo}"' chown -R "$ownership" "/mnt/$file"; done'
749795
fi
750796
751797
step Installing NixOS
@@ -756,27 +802,27 @@ export PATH="\$PATH:/run/current-system/sw/bin"
756802
757803
if [ ! -d "/mnt/tmp" ]; then
758804
# needed for installation if initrd-secrets are used
759-
mkdir -p /mnt/tmp
760-
chmod 777 /mnt/tmp
805+
${maybeSudo} mkdir -p /mnt/tmp
806+
${maybeSudo} chmod 777 /mnt/tmp
761807
fi
762808
763809
if [ ${copyHostKeys-n} = "y" ]; then
764810
# NB we copy host keys that are in turn copied by kexec installer.
765-
mkdir -m 755 -p /mnt/etc/ssh
811+
${maybeSudo} mkdir -m 755 -p /mnt/etc/ssh
766812
for p in /etc/ssh/ssh_host_*; do
767813
# Skip if the source file does not exist (i.e. glob did not match any files)
768814
# or the destination already exists (e.g. copied with --extra-files).
769815
if [ ! -e "\$p" ] || [ -e "/mnt/\$p" ]; then
770816
continue
771817
fi
772-
cp -a "\$p" "/mnt/\$p"
818+
${maybeSudo} cp -a "\$p" "/mnt/\$p"
773819
done
774820
fi
775821
# https://stackoverflow.com/a/13864829
776822
if [ ! -z ${NIXOS_NO_CHECK+0} ]; then
777823
export NIXOS_NO_CHECK
778824
fi
779-
nixos-install --no-root-passwd --no-channel-copy --system "$nixosSystem"
825+
${maybeSudo} nixos-install --no-root-passwd --no-channel-copy --system "$nixosSystem"
780826
SSH
781827
782828
}
@@ -786,11 +832,11 @@ nixosReboot() {
786832
runSsh sh <<SSH
787833
if command -v zpool >/dev/null && [ "\$(zpool list)" != "no pools available" ]; then
788834
# we always want to export the zfs pools so people can boot from it without force import
789-
umount -Rv /mnt/
790-
swapoff -a
791-
zpool export -a || true
835+
${maybeSudo} umount -Rv /mnt/
836+
${maybeSudo} swapoff -a
837+
${maybeSudo} zpool export -a || true
792838
fi
793-
nohup sh -c 'sleep 6 && reboot' >/dev/null &
839+
${maybeSudo} nohup sh -c 'sleep 6 && reboot' >/dev/null &
794840
SSH
795841
796842
step Waiting for the machine to become unreachable due to reboot
@@ -840,7 +886,6 @@ main() {
840886
fi
841887
842888
sshSettings=$(ssh "${sshArgs[@]}" -G "${sshConnection}")
843-
sshUser=$(echo "$sshSettings" | awk '/^user / { print $2 }')
844889
sshHost=$(echo "$sshSettings" | awk '/^hostname / { print $2 }')
845890
846891
uploadSshKey
@@ -898,13 +943,6 @@ main() {
898943
fi
899944
fi
900945
901-
# Installation will fail if non-root user is used for installer.
902-
# Switch to root user by copying authorized_keys.
903-
if [[ ${isInstaller} == "y" ]] && [[ ${sshUser} != "root" ]]; then
904-
# Allow copy to fail if authorized_keys does not exist, like if using /etc/ssh/authorized_keys.d/
905-
runSsh "${maybeSudo} mkdir -p /root/.ssh; ${maybeSudo} cp ~/.ssh/authorized_keys /root/.ssh || true"
906-
sshConnection="root@${sshHost}"
907-
fi
908946
909947
if [[ ${phases[disko]} == 1 ]]; then
910948
runDisko "$diskoScript"

tests/flake-module.nix

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@
77
./modules/system-to-install.nix
88
inputs.disko.nixosModules.disko
99
];
10+
system-to-install-vdb = pkgs.nixos [
11+
./modules/system-to-install.nix
12+
inputs.disko.nixosModules.disko
13+
{
14+
nixos-anywhere.diskDevice = "/dev/vdb";
15+
}
16+
];
1017
testInputsUnstable = {
1118
inherit pkgs;
1219
inherit (inputs.disko.nixosModules) disko;
@@ -17,6 +24,9 @@
1724
testInputsStable = testInputsUnstable // {
1825
kexec-installer = "${inputs'.nixos-images.packages.kexec-installer-nixos-stable-noninteractive}/nixos-kexec-installer-noninteractive-${system}.tar.gz";
1926
};
27+
testInputsInstallerSudo = testInputsUnstable // {
28+
system-to-install = system-to-install-vdb;
29+
};
2030
linuxTestInputs = testInputsUnstable // {
2131
nix-vm-test = inputs.nix-vm-test;
2232
};
@@ -26,6 +36,7 @@
2636
from-nixos-stable = import ./from-nixos.nix testInputsStable;
2737
from-nixos-with-sudo = import ./from-nixos-with-sudo.nix testInputsUnstable;
2838
from-nixos-with-sudo-stable = import ./from-nixos-with-sudo.nix testInputsStable;
39+
from-nixos-installer-with-sudo = import ./from-nixos-installer-with-sudo.nix testInputsInstallerSudo;
2940
from-nixos-with-generated-config = import ./from-nixos-generate-config.nix testInputsUnstable;
3041
from-nixos-build-on-remote = import ./from-nixos-build-on-remote.nix testInputsUnstable;
3142
from-nixos-separated-phases = import ./from-nixos-separated-phases.nix testInputsUnstable;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
(import ./lib/test-base.nix) {
2+
name = "from-nixos-installer-with-sudo";
3+
nodes = {
4+
installer = ./modules/installer.nix;
5+
installed = { modulesPath, ... }: {
6+
imports = [
7+
(modulesPath + "/installer/cd-dvd/installation-cd-base.nix")
8+
];
9+
10+
services.openssh.enable = true;
11+
virtualisation.memorySize = 1500;
12+
virtualisation.emptyDiskImages = [ 1024 ];
13+
14+
users.users.nixos = {
15+
isNormalUser = true;
16+
openssh.authorizedKeys.keyFiles = [ ./modules/ssh-keys/ssh.pub ];
17+
extraGroups = [ "wheel" ];
18+
};
19+
security.sudo.enable = true;
20+
security.sudo.wheelNeedsPassword = false;
21+
22+
# Configure nix trusted users for remote builds with sudo
23+
nix.settings.trusted-users = [ "root" "nixos" ];
24+
};
25+
};
26+
testScript = ''
27+
start_all()
28+
installer.succeed("echo super-secret > /tmp/disk-1.key")
29+
installer.succeed("mkdir -p /tmp/extra-files/var/lib/secrets")
30+
installer.succeed("echo test-value > /tmp/extra-files/var/lib/secrets/test")
31+
32+
output = installer.succeed("""
33+
nixos-anywhere \
34+
-i /root/.ssh/install_key \
35+
--debug \
36+
--phases disko,install \
37+
--disk-encryption-keys /tmp/disk-1.key /tmp/disk-1.key \
38+
--extra-files /tmp/extra-files \
39+
--store-paths /etc/nixos-anywhere/disko /etc/nixos-anywhere/system-to-install \
40+
nixos@installed >&2
41+
echo "disk-1.key: '$(ssh -i /root/.ssh/install_key -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no \
42+
nixos@installed sudo cat /tmp/disk-1.key)'"
43+
echo "extra-file: '$(ssh -i /root/.ssh/install_key -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no \
44+
nixos@installed sudo cat /mnt/var/lib/secrets/test)'"
45+
""")
46+
47+
assert "disk-1.key: 'super-secret'" in output, f"output does not contain expected values: {output}"
48+
assert "extra-file: 'test-value'" in output, f"output does not contain expected values: {output}"
49+
'';
50+
}

tests/modules/system-to-install.nix

Lines changed: 42 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,51 @@
1-
{ modulesPath, self, lib, ... }: {
1+
{ modulesPath, lib, config, ... }:
2+
{
3+
options.nixos-anywhere.diskDevice = lib.mkOption {
4+
type = lib.types.str;
5+
default = "/dev/vda";
6+
description = "The disk device to use for installation";
7+
};
8+
29
imports = [
310
(modulesPath + "/testing/test-instrumentation.nix")
411
(modulesPath + "/profiles/qemu-guest.nix")
512
(modulesPath + "/profiles/minimal.nix")
613
];
7-
networking.hostName = lib.mkDefault "nixos-anywhere";
8-
documentation.enable = false;
9-
hardware.enableAllFirmware = false;
10-
networking.hostId = "8425e349"; # from profiles/base.nix, needed for zfs
11-
boot.zfs.devNodes = "/dev/disk/by-uuid"; # needed because /dev/disk/by-id is empty in qemu-vms
12-
disko.devices = {
13-
disk = {
14-
vda = {
15-
device = "/dev/vda";
16-
type = "disk";
17-
content = {
18-
type = "gpt";
19-
partitions = {
20-
boot = {
21-
size = "1M";
22-
type = "EF02";
23-
};
24-
ESP = {
25-
size = "100M";
26-
type = "EF00";
27-
content = {
28-
type = "filesystem";
29-
format = "vfat";
30-
mountpoint = "/boot";
14+
15+
config = {
16+
networking.hostName = lib.mkDefault "nixos-anywhere";
17+
documentation.enable = false;
18+
hardware.enableAllFirmware = false;
19+
networking.hostId = "8425e349"; # from profiles/base.nix, needed for zfs
20+
boot.zfs.devNodes = "/dev/disk/by-uuid"; # needed because /dev/disk/by-id is empty in qemu-vms
21+
disko.devices = {
22+
disk = {
23+
main = {
24+
device = config.nixos-anywhere.diskDevice;
25+
type = "disk";
26+
content = {
27+
type = "gpt";
28+
partitions = {
29+
boot = {
30+
size = "1M";
31+
type = "EF02";
3132
};
32-
};
33-
root = {
34-
size = "100%";
35-
content = {
36-
type = "filesystem";
37-
format = "ext4";
38-
mountpoint = "/";
33+
ESP = {
34+
size = "100M";
35+
type = "EF00";
36+
content = {
37+
type = "filesystem";
38+
format = "vfat";
39+
mountpoint = "/boot";
40+
};
41+
};
42+
root = {
43+
size = "100%";
44+
content = {
45+
type = "filesystem";
46+
format = "ext4";
47+
mountpoint = "/";
48+
};
3949
};
4050
};
4151
};

0 commit comments

Comments
 (0)