diff --git a/docs/cli.md b/docs/cli.md index 2b2aca67..32caf970 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -20,7 +20,9 @@ Options: print full build logs * --env-password set a password used by ssh-copy-id, the password should be set by - the environment variable SSHPASS + the environment variable SSHPASS. Additionally, sudo password can be set + via SUDO_PASSWORD environment variable for remote sudo operations + (only supported with sudo, not doas) * -s, --store-paths set the store paths to the disko-script and nixos-system directly if this is given, flake is not needed diff --git a/docs/quickstart.md b/docs/quickstart.md index 5eb1a10e..cd8c894d 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -120,7 +120,9 @@ example uses a local directory on the source machine. If your SSH key is not found, you will be asked for your password. If you are using a non-root user, you must have access to sudo without a password. To avoid SSH password prompts, set the `SSHPASS` environment variable to your password -and add `--env-password` to the `nixos-anywhere` command. If providing a +and add `--env-password` to the `nixos-anywhere` command. Additionally, if your +target machine requires a sudo password, you can set the `SUDO_PASSWORD` +environment variable (only supported with sudo, not doas). If providing a specific SSH key through `-i` (identity_file), this key will then be used for the installation and no temporary SSH key will be created. diff --git a/docs/reference.md b/docs/reference.md index cb70c8b8..15a9177e 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -41,7 +41,9 @@ Options: print full build logs * --env-password set a password used by ssh-copy-id, the password should be set by - the environment variable SSHPASS + the environment variable SSHPASS. Additionally, sudo password can be set + via SUDO_PASSWORD environment variable for remote sudo operations + (only supported with sudo, not doas) * -s, --store-paths set the store paths to the disko-script and nixos-system directly if this is given, flake is not needed diff --git a/src/get-facts.sh b/src/get-facts.sh index 80434422..0a4b7778 100755 --- a/src/get-facts.sh +++ b/src/get-facts.sh @@ -16,6 +16,7 @@ hasTar=$(has tar) hasCpio=$(has cpio) hasSudo=$(has sudo) hasDoas=$(has doas) +hasPasswordlessSudo=$(if [ "$(has sudo)" = "y" ] && sudo -n true >/dev/null 2>&1; then echo "y"; else echo "n"; fi) hasWget=$(has wget) hasCurl=$(has curl) hasSetsid=$(has setsid) diff --git a/src/nixos-anywhere.sh b/src/nixos-anywhere.sh index e6746967..9c26613b 100755 --- a/src/nixos-anywhere.sh +++ b/src/nixos-anywhere.sh @@ -21,6 +21,7 @@ nixOptions=( "--no-write-lock-file" ) SSH_PRIVATE_KEY=${SSH_PRIVATE_KEY-} +SUDO_PASSWORD=${SUDO_PASSWORD-} declare -A phases phases[kexec]=1 @@ -53,6 +54,7 @@ hasTar= hasCpio= hasSudo= hasDoas= +hasPasswordlessSudo= hasWget= hasCurl= hasSetsid= @@ -64,7 +66,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() { @@ -91,7 +93,9 @@ Options: print full build logs * --env-password set a password used by ssh-copy-id, the password should be set by - the environment variable SSHPASS + the environment variable SSHPASS. Additionally, sudo password can be set + via SUDO_PASSWORD environment variable for remote sudo operations + (only supported with sudo, not doas). * -s, --store-paths set the store paths to the disko-script and nixos-system directly if this is given, flake is not needed @@ -247,6 +251,10 @@ parseArgs() { ;; --debug) enableDebug="-x" + if [[ ${SUDO_PASSWORD} != "" ]]; then + echo "WARNING: Debug mode enabled with SUDO_PASSWORD. Password authentication may interfere with debug output." >&2 + sleep 2 + fi printBuildLogs=y set -x ;; @@ -420,19 +428,171 @@ runSsh() { ssh "$sshTtyParam" "${sshArgs[@]}" "$sshConnection" "$@" } +# Helper function to authenticate sudo with password if needed +maybeSudo() { + if [[ -n ${SUDO_PASSWORD} ]] && [[ ${maybeSudoCommand} == "sudo" ]]; then + # If debug is enabled and we have a sudo password, warn about potential issues + # Use sudo with password authentication - pipe password to all sudo commands + printf "printf %%s %q | sudo -S " "$SUDO_PASSWORD" + # Restore debug state if it was enabled + elif [[ -n ${maybeSudoCommand} ]]; then + printf '%s ' "${maybeSudoCommand}" + fi + # No output if no sudo needed (e.g., already root after kexec) +} + +# Test and cache sudo password if needed +testAndCacheSudoPassword() { + # Skip if no sudo command available + if [[ -z ${maybeSudoCommand} ]]; then + return 0 + fi + + # Skip if using doas (doesn't support password authentication) + if [[ ${maybeSudoCommand} == "doas" ]]; then + return 0 + fi + + # Skip if sudo works without password + if [[ ${hasPasswordlessSudo} == "y" ]]; then + step "Passwordless sudo confirmed" + return 0 + fi + + # If we already have a password supplied, trust it + if [[ -n ${SUDO_PASSWORD} ]]; then + step "Using supplied sudo password" + return 0 + fi + + # Only prompt for password in interactive sessions + if [[ -t 0 ]]; then + step "Sudo requires password authentication" + local attempts=0 + local maxAttempts=5 + + while [[ $attempts -lt $maxAttempts ]]; do + echo -n "Enter sudo password for ${sshConnection}: " + read -rs password + echo + + # Test the password + local testOutput + testOutput=$(runSshNoTty "echo $(printf %q "$password") | sudo -S echo 'SUDO_TEST_SUCCESS'" 2>&1) + if [[ $testOutput == *"SUDO_TEST_SUCCESS"* ]]; then + SUDO_PASSWORD="$password" + step "Sudo password verified and cached" + return 0 + else + ((attempts++)) + if [[ $attempts -lt $maxAttempts ]]; then + echo "Invalid password, please try again ($attempts/$maxAttempts)" + fi + fi + done + + abort "Failed to authenticate sudo after $maxAttempts attempts" + else + # Non-interactive session without working sudo + abort "Sudo requires password but running in non-interactive mode. Set SUDO_PASSWORD environment variable or configure passwordless sudo." + fi +} + +urlEncode() { + local string="${1}" + local strlen=${#string} + local encoded="" + local pos c o + + for ((pos = 0; pos < strlen; pos++)); do + c=${string:pos:1} + case "$c" in + [-_.~a-zA-Z0-9]) o="${c}" ;; + *) printf -v o '%%%02x' "'$c" ;; + esac + encoded+="${o}" + done + echo "${encoded}" +} + +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 ${maybeSudoCommand} ]] && [[ $storeUrl == ssh-ng://* ]]; then + local remoteProgram + if [[ -n ${SUDO_PASSWORD} ]] && [[ ${maybeSudoCommand} == "sudo" ]]; then + # Use password authentication for nix-daemon + remoteProgram="sh -c $(urlEncode "$(printf %s "$(printf '%q' "$SUDO_PASSWORD")" | sudo -S nix-daemon)")" + else + remoteProgram="${maybeSudoCommand} nix-daemon" + fi + + if [[ $storeUrl == *"?"* ]]; then + storeUrl="${storeUrl}&remote-program=${remoteProgram}" + else + storeUrl="${storeUrl}?remote-program=${remoteProgram}" + 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() { @@ -518,7 +678,7 @@ importFacts() { # shellcheck disable=SC2046 export $(echo "$filteredFacts" | xargs) - for var in isOs isArch isKexec isInstaller isContainer hasIpv6Only hasTar hasCpio hasSudo hasDoas hasWget hasCurl hasSetsid; do + for var in isOs isArch isKexec isInstaller isContainer hasIpv6Only hasTar hasCpio hasSudo hasDoas hasPasswordlessSudo hasWget hasCurl hasSetsid; do if [[ -z ${!var} ]]; then abort "Failed to retrieve fact $var from host" fi @@ -569,7 +729,6 @@ checkBuildLocally() { } generateHardwareConfig() { - local maybeSudo="$maybeSudo" mkdir -p "$(dirname "$hardwareConfigPath")" case "$hardwareConfigBackend" in nixos-facter) @@ -577,12 +736,10 @@ generateHardwareConfig() { if [[ ${hasNixOSFacter} == "n" ]]; then 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" fi - else - maybeSudo="" fi step "Generating hardware-configuration.nix using nixos-facter" - runSshNoTty -o ConnectTimeout=10 ${maybeSudo} "nixos-facter" >"$hardwareConfigPath" + runSshNoTty -o ConnectTimeout=10 "$(maybeSudo)nixos-facter" >"$hardwareConfigPath" ;; nixos-generate-config) step "Generating hardware-configuration.nix using nixos-generate-config" @@ -636,10 +793,10 @@ runKexec() { local remoteCommandTemplate remoteCommandTemplate=" set -eu ${enableDebug} -${maybeSudo} rm -rf /root/kexec -${maybeSudo} mkdir -p /root/kexec +$(maybeSudo)rm -rf /root/kexec +$(maybeSudo)mkdir -p /root/kexec %TAR_COMMAND% -TMPDIR=/root/kexec setsid --wait ${maybeSudo} /root/kexec/kexec/run --kexec-extra-flags $(printf '%q ' "$kexecExtraFlags") +$(maybeSudo)TMPDIR=/root/kexec setsid --wait /root/kexec/kexec/run${kexecExtraFlags:+ --kexec-extra-flags \"$kexecExtraFlags\"} " # Define upload commands @@ -659,16 +816,17 @@ TMPDIR=/root/kexec setsid --wait ${maybeSudo} /root/kexec/kexec/run --kexec-extr local tarCommand local remoteCommands + if [[ ${#localUploadCommand[@]} -eq 0 ]]; then # Use remote command for download and execution - tarCommand="$(printf '%q ' "${remoteUploadCommand[@]}") | ${maybeSudo} tar -C /root/kexec -xvzf-" + tarCommand="$(printf '%q ' "${remoteUploadCommand[@]}") | ${maybeSudoCommand} tar -C /root/kexec -xvzf-" remoteCommands=${remoteCommandTemplate//'%TAR_COMMAND%'/$tarCommand} runSsh sh -c "$(printf '%q' "$remoteCommands")" else # Use local command with pipe to remote - tarCommand="${maybeSudo} tar -C /root/kexec -xvzf-" + tarCommand="${maybeSudoCommand} tar -C /root/kexec -xvzf-" remoteCommands=${remoteCommandTemplate//'%TAR_COMMAND%'/$tarCommand} "${localUploadCommand[@]}" | runSsh sh -c "$(printf '%q' "$remoteCommands")" @@ -688,6 +846,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 + maybeSudoCommand="" # waiting for machine to become available again until runSsh -o ConnectTimeout=10 -- exit 0; do sleep 5; done @@ -697,58 +857,58 @@ 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 "${maybeSudoCommand} 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' + # shellcheck disable=SC2016,SC2086 + printf "%s\n" "${!extraFilesOwnership[@]}" "${extraFilesOwnership[@]}" | pr -2t | runSsh "while read file ownership; do $(maybeSudo)chown -R \$ownership /mnt/\$file; done" fi step Installing NixOS + # shellcheck disable=SC2016 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 +1000,6 @@ main() { fi sshSettings=$(ssh "${sshArgs[@]}" -G "${sshConnection}") - sshUser=$(echo "$sshSettings" | awk '/^user / { print $2 }') sshHost=$(echo "$sshSettings" | awk '/^hostname / { print $2 }') uploadSshKey @@ -859,13 +1018,19 @@ main() { abort "no setsid command found, but required to run the kexec script under a new session" fi - maybeSudo="" + maybeSudoCommand="" if [[ ${hasSudo-n} == "y" ]]; then - maybeSudo="sudo" + maybeSudoCommand="sudo" elif [[ ${hasDoas-n} == "y" ]]; then - maybeSudo="doas" + maybeSudoCommand="doas" + if [[ -n ${SUDO_PASSWORD} ]]; then + abort "SUDO_PASSWORD environment variable is not supported with doas. Please configure passwordless doas or use sudo instead." + fi fi + # Test and cache sudo password if needed + testAndCacheSudoPassword + if [[ ${isOs} != "Linux" ]]; then abort "This script requires Linux as the operating system, but got $isOs" fi @@ -898,14 +1063,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/terraform/all-in-one.md b/terraform/all-in-one.md index bac0ac3a..ef41c0d0 100644 --- a/terraform/all-in-one.md +++ b/terraform/all-in-one.md @@ -28,6 +28,10 @@ module "deploy" { # debug_logging = true # build the closure on the remote machine instead of locally # build_on_remote = true + # Optional: SSH password for initial installation + # install_pass = "your-ssh-password" + # Optional: Sudo password for remote operations during installation + # install_sudo_pass = "your-sudo-password" # script is below extra_files_script = "${path.module}/decrypt-ssh-secrets.sh" disk_encryption_key_scripts = [{ @@ -139,7 +143,7 @@ locals { resource "local_file" "nixos_vars" { content = jsonencode(local.nixos_vars) # Converts variables to JSON filename = local.nixos_vars_file # Specifies the output file path - file_permission = "600" + file_permission = "600" # Automatically adds the generated file to Git provisioner "local-exec" { @@ -209,8 +213,10 @@ No resources. | [extra\_files\_script](#input_extra_files_script) | A script that should place files in the current directory that will be copied to the targets / directory | `string` | `null` | no | | [file](#input_file) | Nix file containing the nixos\_system\_attr and nixos\_partitioner\_attr. Use this if you are not using flake | `string` | `null` | no | | [install\_bootloader](#input_install_bootloader) | Install/re-install the bootloader | `bool` | `false` | no | +| [install\_pass](#input_install_pass) | Password used to connect to the target\_host during installation | `string` | `null` | no | | [install\_port](#input_install_port) | SSH port used to connect to the target\_host, before installing NixOS. If null than the value of `target_port` is used | `string` | `null` | no | | [install\_ssh\_key](#input_install_ssh_key) | Content of private key used to connect to the target\_host during initial installation | `string` | `null` | no | +| [install\_sudo\_pass](#input_install_sudo_pass) | Sudo password for remote sudo operations during installation. Only supported with sudo, not doas. | `string` | `null` | no | | [install\_user](#input_install_user) | SSH user used to connect to the target\_host, before installing NixOS. If null than the value of `target_host` is used | `string` | `null` | no | | [instance\_id](#input_instance_id) | The instance id of the target\_host, used to track when to reinstall the machine | `string` | `null` | no | | [kexec\_tarball\_url](#input_kexec_tarball_url) | NixOS kexec installer tarball url | `string` | `null` | no | diff --git a/terraform/all-in-one/main.tf b/terraform/all-in-one/main.tf index fd4ec71d..aba9b2cc 100644 --- a/terraform/all-in-one/main.tf +++ b/terraform/all-in-one/main.tf @@ -27,6 +27,8 @@ module "install" { target_user = local.install_user target_host = var.target_host target_port = local.install_port + target_pass = var.install_pass + target_sudo_pass = var.install_sudo_pass nixos_partitioner = module.partitioner-build.result.out nixos_system = module.system-build.result.out ssh_private_key = var.install_ssh_key diff --git a/terraform/all-in-one/variables.tf b/terraform/all-in-one/variables.tf index 4cc33757..00499d39 100644 --- a/terraform/all-in-one/variables.tf +++ b/terraform/all-in-one/variables.tf @@ -149,3 +149,17 @@ variable "install_bootloader" { description = "Install/re-install the bootloader" default = false } + +variable "install_pass" { + type = string + description = "Password used to connect to the target_host during installation" + default = null + sensitive = true +} + +variable "install_sudo_pass" { + type = string + description = "Sudo password for remote sudo operations during installation. Only supported with sudo, not doas." + default = null + sensitive = true +} diff --git a/terraform/install.md b/terraform/install.md index 7494c2ce..073c70ab 100644 --- a/terraform/install.md +++ b/terraform/install.md @@ -34,6 +34,10 @@ module "install" { nixos_system = module.system-build.result.out nixos_partitioner = module.disko.result.out target_host = local.ipv4 + # Optional: SSH password authentication + # target_pass = "your-ssh-password" + # Optional: Sudo password for remote operations + # target_sudo_pass = "your-sudo-password" } ``` @@ -82,6 +86,7 @@ No modules. | [target\_host](#input_target_host) | DNS host to deploy to | `string` | n/a | yes | | [target\_pass](#input_target_pass) | Password used to connect to the target\_host | `string` | `null` | no | | [target\_port](#input_target_port) | SSH port used to connect to the target\_host | `number` | `22` | no | +| [target\_sudo\_pass](#input_target_sudo_pass) | Sudo password for remote sudo operations on target\_host. Only supported with sudo, not doas. | `string` | `null` | no | | [target\_user](#input_target_user) | SSH user used to connect to the target\_host | `string` | `"root"` | no | ## Outputs diff --git a/terraform/install/main.tf b/terraform/install/main.tf index 175da60a..81d441f7 100644 --- a/terraform/install/main.tf +++ b/terraform/install/main.tf @@ -12,6 +12,7 @@ locals { target_host = var.target_host target_port = var.target_port target_pass = var.target_pass + target_sudo_pass = var.target_sudo_pass extra_files_script = var.extra_files_script build_on_remote = var.build_on_remote flake = var.flake diff --git a/terraform/install/run-nixos-anywhere.sh b/terraform/install/run-nixos-anywhere.sh index 1d259a1e..76fd11d3 100755 --- a/terraform/install/run-nixos-anywhere.sh +++ b/terraform/install/run-nixos-anywhere.sh @@ -13,7 +13,14 @@ args=() if [[ ${input[debug_logging]} == "true" ]]; then set -x - declare -p input + # Print input variables but filter out sensitive passwords + for key in "${!input[@]}"; do + if [[ $key == *"pass"* ]]; then + echo "input[$key]='[FILTERED]'" + else + echo "input[$key]='${input[$key]}'" + fi + done args+=("--debug") fi if [[ ${input[kexec_tarball_url]} != "null" ]]; then @@ -40,9 +47,22 @@ args+=(--phases "${input[phases]}") if [[ ${input[ssh_private_key]} != null ]]; then export SSH_PRIVATE_KEY="${input[ssh_private_key]}" fi +if [[ ${input[target_pass]} != null || ${input[target_sudo_pass]} != null ]]; then + args+=("--env-password") +fi +# Temporarily disable debug output when exporting sensitive variables +if [[ ${input[debug_logging]} == "true" ]]; then + set +x +fi if [[ ${input[target_pass]} != null ]]; then export SSHPASS=${input[target_pass]} - args+=("--env-password") +fi +if [[ ${input[target_sudo_pass]} != null ]]; then + export SUDO_PASSWORD=${input[target_sudo_pass]} +fi +# Re-enable debug output if it was enabled +if [[ ${input[debug_logging]} == "true" ]]; then + set -x fi tmpdir=$(mktemp -d) diff --git a/terraform/install/variables.tf b/terraform/install/variables.tf index 7caec0f8..4afa188f 100644 --- a/terraform/install/variables.tf +++ b/terraform/install/variables.tf @@ -41,6 +41,13 @@ variable "target_pass" { default = null } +variable "target_sudo_pass" { + type = string + description = "Sudo password for remote sudo operations on target_host. Only supported with sudo, not doas." + default = null + sensitive = true +} + variable "ssh_private_key" { type = string description = "Content of private key used to connect to the target_host" 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/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 """) ''; } diff --git a/tests/from-nixos-with-sudo.nix b/tests/from-nixos-with-sudo.nix index aaf0465a..3e87a5b4 100644 --- a/tests/from-nixos-with-sudo.nix +++ b/tests/from-nixos-with-sudo.nix @@ -8,18 +8,19 @@ users.users.nixos = { isNormalUser = true; + password = "test123"; openssh.authorizedKeys.keyFiles = [ ./modules/ssh-keys/ssh.pub ]; extraGroups = [ "wheel" ]; }; security.sudo.enable = true; - security.sudo.wheelNeedsPassword = false; + security.sudo.wheelNeedsPassword = true; }; }; testScript = '' start_all() installer.succeed("echo super-secret > /tmp/disk-1.key") output = installer.succeed(""" - nixos-anywhere \ + SUDO_PASSWORD=test123 nixos-anywhere \ -i /root/.ssh/install_key \ --debug \ --kexec /etc/nixos-anywhere/kexec-installer \ @@ -27,7 +28,7 @@ --disk-encryption-keys /tmp/disk-1.key /tmp/disk-1.key \ --disk-encryption-keys /tmp/disk-2.key <(echo another-secret) \ --store-paths /etc/nixos-anywhere/disko /etc/nixos-anywhere/system-to-install \ - nixos@installed >&2 + nixos@installed 2>&1 echo "disk-1.key: '$(ssh -i /root/.ssh/install_key -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no \ root@installed cat /tmp/disk-1.key)'" echo "disk-2.key: '$(ssh -i /root/.ssh/install_key -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no \ diff --git a/tests/from-nixos.nix b/tests/from-nixos.nix index c828adec..d7e850f1 100644 --- a/tests/from-nixos.nix +++ b/tests/from-nixos.nix @@ -40,7 +40,7 @@ installer.succeed("chmod 600 /tmp/extra-files/home/user/.ssh/id_ed25519") ssh_key_path = "/etc/ssh/ssh_host_ed25519_key.pub" ssh_key_output = installer.wait_until_succeeds(f""" - ssh -i /root/.ssh/install_key -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no \ + timeout 3 ssh -i /root/.ssh/install_key -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no \ root@installed cat {ssh_key_path} """) installer.succeed(""" 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 = "/"; + }; }; }; };