@@ -21,6 +21,7 @@ nixOptions=(
2121 " --no-write-lock-file"
2222)
2323SSH_PRIVATE_KEY=${SSH_PRIVATE_KEY-}
24+ SUDO_PASSWORD=${SUDO_PASSWORD-}
2425
2526declare -A phases
2627phases[kexec]=1
@@ -53,6 +54,7 @@ hasTar=
5354hasCpio=
5455hasSudo=
5556hasDoas=
57+ hasPasswordlessSudo=
5658hasWget=
5759hasCurl=
5860hasSetsid=
@@ -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+
423531buildStoreUrl () {
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
628744generateHardwareConfig () {
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="
695808set -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
779893nixosInstall () {
@@ -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
811926set -eu ${enableDebug}
812927# when running not in nixos we might miss this directory, but it's needed in the nixos chroot during installation
813928export PATH="\$ PATH:/run/current-system/sw/bin"
814929
815930if [ ! -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)
819934fi
820935
821936if [ ${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
832947fi
833948# https://stackoverflow.com/a/13864829
834949if [ ! -z ${NIXOS_NO_CHECK+0} ]; then
835950 export NIXOS_NO_CHECK
836951fi
837- ${ maybeSudo} nixos-install --no-root-passwd --no-channel-copy --system "$nixosSystem "
952+ $( maybeSudo nixos-install --no-root-passwd --no-channel-copy --system " $nixosSystem " )
838953SSH
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 &
852967SSH
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