Skip to content

Commit 385964a

Browse files
committed
add support for providing sudo passwords via environemnt variables
1 parent 48f06de commit 385964a

File tree

8 files changed

+170
-38
lines changed

8 files changed

+170
-38
lines changed

docs/cli.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ Options:
2020
print full build logs
2121
* --env-password
2222
set a password used by ssh-copy-id, the password should be set by
23-
the environment variable SSHPASS
23+
the environment variable SSHPASS. Additionally, sudo password can be set
24+
via SUDO_PASSWORD environment variable for remote sudo operations
25+
(only supported with sudo, not doas)
2426
* -s, --store-paths <disko-script> <nixos-system>
2527
set the store paths to the disko-script and nixos-system directly
2628
if this is given, flake is not needed

docs/quickstart.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,9 @@ example uses a local directory on the source machine.
120120
If your SSH key is not found, you will be asked for your password. If you are
121121
using a non-root user, you must have access to sudo without a password. To avoid
122122
SSH password prompts, set the `SSHPASS` environment variable to your password
123-
and add `--env-password` to the `nixos-anywhere` command. If providing a
123+
and add `--env-password` to the `nixos-anywhere` command. Additionally, if your
124+
target machine requires a sudo password, you can set the `SUDO_PASSWORD`
125+
environment variable (only supported with sudo, not doas). If providing a
124126
specific SSH key through `-i` (identity_file), this key will then be used for
125127
the installation and no temporary SSH key will be created.
126128

docs/reference.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ Options:
4141
print full build logs
4242
* --env-password
4343
set a password used by ssh-copy-id, the password should be set by
44-
the environment variable SSHPASS
44+
the environment variable SSHPASS. Additionally, sudo password can be set
45+
via SUDO_PASSWORD environment variable for remote sudo operations
46+
(only supported with sudo, not doas)
4547
* -s, --store-paths <disko-script> <nixos-system>
4648
set the store paths to the disko-script and nixos-system directly
4749
if this is given, flake is not needed

src/get-facts.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ hasTar=$(has tar)
1616
hasCpio=$(has cpio)
1717
hasSudo=$(has sudo)
1818
hasDoas=$(has doas)
19+
hasPasswordlessSudo=$(if [ "$(has sudo)" = "y" ] && sudo -n true >/dev/null 2>&1; then echo "y"; else echo "n"; fi)
1920
hasWget=$(has wget)
2021
hasCurl=$(has curl)
2122
hasSetsid=$(has setsid)

src/nixos-anywhere.sh

Lines changed: 153 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ nixOptions=(
2121
"--no-write-lock-file"
2222
)
2323
SSH_PRIVATE_KEY=${SSH_PRIVATE_KEY-}
24+
SUDO_PASSWORD=${SUDO_PASSWORD-}
2425

2526
declare -A phases
2627
phases[kexec]=1
@@ -53,6 +54,7 @@ hasTar=
5354
hasCpio=
5455
hasSudo=
5556
hasDoas=
57+
hasPasswordlessSudo=
5658
hasWget=
5759
hasCurl=
5860
hasSetsid=
@@ -91,7 +93,9 @@ Options:
9193
print full build logs
9294
* --env-password
9395
set a password used by ssh-copy-id, the password should be set by
94-
the environment variable SSHPASS
96+
the environment variable SSHPASS. Additionally, sudo password can be set
97+
via SUDO_PASSWORD environment variable for remote sudo operations
98+
(only supported with sudo, not doas).
9599
* -s, --store-paths <disko-script> <nixos-system>
96100
set the store paths to the disko-script and nixos-system directly
97101
if this is given, flake is not needed
@@ -247,6 +251,10 @@ parseArgs() {
247251
;;
248252
--debug)
249253
enableDebug="-x"
254+
if [[ ${SUDO_PASSWORD} != "" ]]; then
255+
echo "WARNING: Debug mode enabled with SUDO_PASSWORD. Password authentication may interfere with debug output." >&2
256+
sleep 2
257+
fi
250258
printBuildLogs=y
251259
set -x
252260
;;
@@ -420,6 +428,106 @@ runSsh() {
420428
ssh "$sshTtyParam" "${sshArgs[@]}" "$sshConnection" "$@"
421429
}
422430

431+
# Helper function to authenticate sudo with password if needed
432+
maybeSudo() {
433+
# Early return if no command provided and no sudo password
434+
if [[ $# -eq 0 && -z ${SUDO_PASSWORD} ]]; then
435+
return
436+
fi
437+
438+
# Use 'true' as default command if none provided but we have sudo password
439+
local cmd=("${@:-true}")
440+
441+
if [[ -n ${SUDO_PASSWORD} ]] && [[ ${maybeSudoCommand} == "sudo" ]]; then
442+
# If debug is enabled and we have a sudo password, warn about potential issues
443+
444+
# Use sudo with password authentication - pipe password to all sudo commands
445+
printf "printf %%s %q | sudo -S " "$SUDO_PASSWORD"
446+
printf '%q ' "${cmd[@]}"
447+
elif [[ -n ${maybeSudoCommand} ]]; then
448+
printf '%s ' "${maybeSudoCommand}"
449+
printf '%q ' "${cmd[@]}"
450+
else
451+
# No sudo command needed (e.g., already root after kexec)
452+
printf '%q ' "${cmd[@]}"
453+
fi
454+
echo
455+
}
456+
457+
# Test and cache sudo password if needed
458+
testAndCacheSudoPassword() {
459+
# Skip if no sudo command available
460+
if [[ -z ${maybeSudoCommand} ]]; then
461+
return 0
462+
fi
463+
464+
# Skip if using doas (doesn't support password authentication)
465+
if [[ ${maybeSudoCommand} == "doas" ]]; then
466+
return 0
467+
fi
468+
469+
# Skip if sudo works without password
470+
if [[ ${hasPasswordlessSudo} == "y" ]]; then
471+
step "Passwordless sudo confirmed"
472+
return 0
473+
fi
474+
475+
# If we already have a password supplied, trust it
476+
if [[ -n ${SUDO_PASSWORD} ]]; then
477+
step "Using supplied sudo password"
478+
return 0
479+
fi
480+
481+
# Only prompt for password in interactive sessions
482+
if [[ -t 0 ]]; then
483+
step "Sudo requires password authentication"
484+
local attempts=0
485+
local maxAttempts=5
486+
487+
while [[ $attempts -lt $maxAttempts ]]; do
488+
echo -n "Enter sudo password for ${sshConnection}: "
489+
read -rs password
490+
echo
491+
492+
# Test the password
493+
local testOutput
494+
testOutput=$(runSshNoTty "printf '%s' $(printf '%q' "$password") | sudo -S echo 'SUDO_TEST_SUCCESS'" 2>&1)
495+
if [[ $testOutput == "SUDO_TEST_SUCCESS" ]]; then
496+
SUDO_PASSWORD="$password"
497+
step "Sudo password verified and cached"
498+
return 0
499+
else
500+
((attempts++))
501+
if [[ $attempts -lt $maxAttempts ]]; then
502+
echo "Invalid password, please try again ($attempts/$maxAttempts)"
503+
fi
504+
fi
505+
done
506+
507+
abort "Failed to authenticate sudo after $maxAttempts attempts"
508+
else
509+
# Non-interactive session without working sudo
510+
abort "Sudo requires password but running in non-interactive mode. Set SUDO_PASSWORD environment variable or configure passwordless sudo."
511+
fi
512+
}
513+
514+
urlEncode() {
515+
local string="${1}"
516+
local strlen=${#string}
517+
local encoded=""
518+
local pos c o
519+
520+
for ((pos = 0; pos < strlen; pos++)); do
521+
c=${string:pos:1}
522+
case "$c" in
523+
[-_.~a-zA-Z0-9]) o="${c}" ;;
524+
*) printf -v o '%%%02x' "'$c" ;;
525+
esac
526+
encoded+="${o}"
527+
done
528+
echo "${encoded}"
529+
}
530+
423531
buildStoreUrl() {
424532
local storeUrl="$1"
425533

@@ -433,11 +541,19 @@ buildStoreUrl() {
433541
fi
434542

435543
# Add remote-program parameter when sudo is needed
436-
if [[ -n ${maybeSudo} ]] && [[ $storeUrl == ssh-ng://* ]]; then
544+
if [[ -n ${maybeSudoCommand} ]] && [[ $storeUrl == ssh-ng://* ]]; then
545+
local remoteProgram
546+
if [[ -n ${SUDO_PASSWORD} ]] && [[ ${maybeSudoCommand} == "sudo" ]]; then
547+
# Use password authentication for nix-daemon
548+
remoteProgram="sh,-c,$(urlEncode "$(printf %s "$(printf '%q' "$SUDO_PASSWORD")" | sudo -S nix-daemon)")"
549+
else
550+
remoteProgram="${maybeSudoCommand},nix-daemon"
551+
fi
552+
437553
if [[ $storeUrl == *"?"* ]]; then
438-
storeUrl="${storeUrl}&remote-program=${maybeSudo} nix-daemon"
554+
storeUrl="${storeUrl}&remote-program=${remoteProgram}"
439555
else
440-
storeUrl="${storeUrl}?remote-program=${maybeSudo} nix-daemon"
556+
storeUrl="${storeUrl}?remote-program=${remoteProgram}"
441557
fi
442558
fi
443559

@@ -575,7 +691,7 @@ importFacts() {
575691
# shellcheck disable=SC2046
576692
export $(echo "$filteredFacts" | xargs)
577693

578-
for var in isOs isArch isKexec isInstaller isContainer hasIpv6Only hasTar hasCpio hasSudo hasDoas hasWget hasCurl hasSetsid; do
694+
for var in isOs isArch isKexec isInstaller isContainer hasIpv6Only hasTar hasCpio hasSudo hasDoas hasPasswordlessSudo hasWget hasCurl hasSetsid; do
579695
if [[ -z ${!var} ]]; then
580696
abort "Failed to retrieve fact $var from host"
581697
fi
@@ -626,20 +742,17 @@ checkBuildLocally() {
626742
}
627743
628744
generateHardwareConfig() {
629-
local maybeSudo="$maybeSudo"
630745
mkdir -p "$(dirname "$hardwareConfigPath")"
631746
case "$hardwareConfigBackend" in
632747
nixos-facter)
633748
if [[ ${isInstaller} == "y" ]]; then
634749
if [[ ${hasNixOSFacter} == "n" ]]; then
635750
abort "nixos-facter is not available in booted installer, use nixos-generate-config. For nixos-facter, you may want to boot an installer image from here instead: https://github.com/nix-community/nixos-images"
636751
fi
637-
else
638-
maybeSudo=""
639752
fi
640753
641754
step "Generating hardware-configuration.nix using nixos-facter"
642-
runSshNoTty -o ConnectTimeout=10 ${maybeSudo} "nixos-facter" >"$hardwareConfigPath"
755+
runSshNoTty -o ConnectTimeout=10 "$(maybeSudo nixos-facter)" >"$hardwareConfigPath"
643756
;;
644757
nixos-generate-config)
645758
step "Generating hardware-configuration.nix using nixos-generate-config"
@@ -693,10 +806,10 @@ runKexec() {
693806
local remoteCommandTemplate
694807
remoteCommandTemplate="
695808
set -eu ${enableDebug}
696-
${maybeSudo} rm -rf /root/kexec
697-
${maybeSudo} mkdir -p /root/kexec
809+
$(maybeSudo rm -rf /root/kexec)
810+
$(maybeSudo mkdir -p /root/kexec)
698811
%TAR_COMMAND%
699-
TMPDIR=/root/kexec setsid --wait ${maybeSudo} /root/kexec/kexec/run --kexec-extra-flags $(printf '%q ' "$kexecExtraFlags")
812+
$(maybeSudo TMPDIR=/root/kexec setsid --wait /root/kexec/kexec/run --kexec-extra-flags "$kexecExtraFlags")
700813
"
701814
702815
# Define upload commands
@@ -716,16 +829,17 @@ TMPDIR=/root/kexec setsid --wait ${maybeSudo} /root/kexec/kexec/run --kexec-extr
716829
717830
local tarCommand
718831
local remoteCommands
832+
719833
if [[ ${#localUploadCommand[@]} -eq 0 ]]; then
720834
# Use remote command for download and execution
721-
tarCommand="$(printf '%q ' "${remoteUploadCommand[@]}") | ${maybeSudo} tar -C /root/kexec -xvzf-"
835+
tarCommand="$(printf '%q ' "${remoteUploadCommand[@]}") | ${maybeSudoCommand} tar -C /root/kexec -xvzf-"
722836
723837
remoteCommands=${remoteCommandTemplate//'%TAR_COMMAND%'/$tarCommand}
724838
725839
runSsh sh -c "$(printf '%q' "$remoteCommands")"
726840
else
727841
# Use local command with pipe to remote
728-
tarCommand="${maybeSudo} tar -C /root/kexec -xvzf-"
842+
tarCommand="${maybeSudoCommand} tar -C /root/kexec -xvzf-"
729843
remoteCommands=${remoteCommandTemplate//'%TAR_COMMAND%'/$tarCommand}
730844
731845
"${localUploadCommand[@]}" | runSsh sh -c "$(printf '%q' "$remoteCommands")"
@@ -746,7 +860,7 @@ TMPDIR=/root/kexec setsid --wait ${maybeSudo} /root/kexec/kexec/run --kexec-extr
746860
# After kexec we explicitly set the user to root@
747861
sshConnection="root@${sshHost}"
748862
# After kexec, we're running as root in the NixOS installer, so no need for sudo
749-
maybeSudo=""
863+
maybeSudoCommand=""
750864
751865
# waiting for machine to become available again
752866
until runSsh -o ConnectTimeout=10 -- exit 0; do sleep 5; done
@@ -756,7 +870,7 @@ runDisko() {
756870
local diskoScript=$1
757871
for path in "${!diskEncryptionKeys[@]}"; do
758872
step "Uploading ${diskEncryptionKeys[$path]} to $path"
759-
runSsh "${maybeSudo} sh -c $(printf '%q' "umask 077; mkdir -p $(dirname "$path"); cat > $path")" <"${diskEncryptionKeys[$path]}"
873+
runSsh "$(maybeSudo sh) -c $(printf '%q' "umask 077; mkdir -p $(dirname "$path"); cat > $path")" <"${diskEncryptionKeys[$path]}"
760874
done
761875
if [[ -n ${diskoScript} ]]; then
762876
nixCopy --to "ssh-ng://$sshConnection" "$diskoScript"
@@ -773,7 +887,7 @@ runDisko() {
773887
fi
774888
775889
step Formatting hard drive with disko
776-
runSsh "${maybeSudo} $diskoScript"
890+
runSsh "$(maybeSudo "$diskoScript")"
777891
}
778892
779893
nixosInstall() {
@@ -796,45 +910,46 @@ nixosInstall() {
796910
797911
if [[ -n ${extraFiles} ]]; then
798912
step Copying extra files
799-
tar -C "$extraFiles" -cpf- . | runSsh "${maybeSudo} tar -C /mnt -xf- --no-same-owner"
913+
tar -C "$extraFiles" -cpf- . | runSsh "${maybeSudoCommand} tar -C /mnt -xf- --no-same-owner"
800914
801-
runSsh "${maybeSudo} chmod 755 /mnt" # tar also changes permissions of /mnt
915+
runSsh "$(maybeSudo chmod 755 /mnt)" # tar also changes permissions of /mnt
802916
fi
803917
804918
if [[ ${#extraFilesOwnership[@]} -gt 0 ]]; then
805919
# shellcheck disable=SC2016
806-
printf "%s\n" "${!extraFilesOwnership[@]}" "${extraFilesOwnership[@]}" | pr -2t | runSsh 'while read file ownership; do '"${maybeSudo}"' chown -R "$ownership" "/mnt/$file"; done'
920+
printf "%s\n" "${!extraFilesOwnership[@]}" "${extraFilesOwnership[@]}" | pr -2t | runSsh 'while read file ownership; do '"$(maybeSudo chown -R \$ownership \"/mnt/\$file\")"'; done'
807921
fi
808922
809923
step Installing NixOS
924+
# shellcheck disable=SC2016
810925
runSsh sh <<SSH
811926
set -eu ${enableDebug}
812927
# when running not in nixos we might miss this directory, but it's needed in the nixos chroot during installation
813928
export PATH="\$PATH:/run/current-system/sw/bin"
814929
815930
if [ ! -d "/mnt/tmp" ]; then
816931
# needed for installation if initrd-secrets are used
817-
${maybeSudo} mkdir -p /mnt/tmp
818-
${maybeSudo} chmod 777 /mnt/tmp
932+
$(maybeSudo mkdir -p /mnt/tmp)
933+
$(maybeSudo chmod 777 /mnt/tmp)
819934
fi
820935
821936
if [ ${copyHostKeys-n} = "y" ]; then
822937
# NB we copy host keys that are in turn copied by kexec installer.
823-
${maybeSudo} mkdir -m 755 -p /mnt/etc/ssh
938+
$(maybeSudo mkdir -m 755 -p /mnt/etc/ssh)
824939
for p in /etc/ssh/ssh_host_*; do
825940
# Skip if the source file does not exist (i.e. glob did not match any files)
826941
# or the destination already exists (e.g. copied with --extra-files).
827942
if [ ! -e "\$p" ] || [ -e "/mnt/\$p" ]; then
828943
continue
829944
fi
830-
${maybeSudo} cp -a "\$p" "/mnt/\$p"
945+
$(maybeSudo cp -a '$p' '/mnt/$p')
831946
done
832947
fi
833948
# https://stackoverflow.com/a/13864829
834949
if [ ! -z ${NIXOS_NO_CHECK+0} ]; then
835950
export NIXOS_NO_CHECK
836951
fi
837-
${maybeSudo} nixos-install --no-root-passwd --no-channel-copy --system "$nixosSystem"
952+
$(maybeSudo nixos-install --no-root-passwd --no-channel-copy --system "$nixosSystem")
838953
SSH
839954
840955
}
@@ -844,11 +959,11 @@ nixosReboot() {
844959
runSsh sh <<SSH
845960
if command -v zpool >/dev/null && [ "\$(zpool list)" != "no pools available" ]; then
846961
# we always want to export the zfs pools so people can boot from it without force import
847-
${maybeSudo} umount -Rv /mnt/
848-
${maybeSudo} swapoff -a
849-
${maybeSudo} zpool export -a || true
962+
$(maybeSudo umount -Rv /mnt/)
963+
$(maybeSudo swapoff -a)
964+
$(maybeSudo zpool export -a || true)
850965
fi
851-
${maybeSudo} nohup sh -c 'sleep 6 && reboot' >/dev/null &
966+
$(maybeSudo nohup sh -c 'sleep 6 && reboot') >/dev/null &
852967
SSH
853968
854969
step Waiting for the machine to become unreachable due to reboot
@@ -916,13 +1031,19 @@ main() {
9161031
abort "no setsid command found, but required to run the kexec script under a new session"
9171032
fi
9181033
919-
maybeSudo=""
1034+
maybeSudoCommand=""
9201035
if [[ ${hasSudo-n} == "y" ]]; then
921-
maybeSudo="sudo"
1036+
maybeSudoCommand="sudo"
9221037
elif [[ ${hasDoas-n} == "y" ]]; then
923-
maybeSudo="doas"
1038+
maybeSudoCommand="doas"
1039+
if [[ -n ${SUDO_PASSWORD} ]]; then
1040+
abort "SUDO_PASSWORD environment variable is not supported with doas. Please configure passwordless doas or use sudo instead."
1041+
fi
9241042
fi
9251043
1044+
# Test and cache sudo password if needed
1045+
testAndCacheSudoPassword
1046+
9261047
if [[ ${isOs} != "Linux" ]]; then
9271048
abort "This script requires Linux as the operating system, but got $isOs"
9281049
fi

0 commit comments

Comments
 (0)