From 1ab74e1222496d26881adcecfc5b30f2c35dd984 Mon Sep 17 00:00:00 2001 From: sntx Date: Wed, 1 Oct 2025 14:49:35 +0200 Subject: [PATCH] feat: option to read sudo password for nodes from environment Reads the passwords on a per-node basis with the environment variable prefixed by `DEPLOY_RS_SUDO_PASSWORD_` and followed by the node name in capital letters. Dashes get replaced with underscores. For example: `DEPLOY_RS_SUDO_PASSWORD_MY_SYSTEM` --- README.md | 6 ++++++ interface.json | 3 +++ src/cli.rs | 22 +++++++++++++++++++++- src/data.rs | 2 ++ src/deploy.rs | 10 +++++----- src/lib.rs | 4 ++++ 6 files changed, 41 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c353203a..c633d399 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,12 @@ This is a set of options that can be put in any of the above definitions, with t # This defaults to `false` interactiveSudo = false; + # Whether to enable reading the sudo password from the environment (password based sudo). Useful when using non-root sshUsers. + # It reads the passwords on a per-node basis with the environment variable prefixed by DEPLOY_RS_SUDO_PASSWORD_ and followed by the node name in capital letters, dashes get replaced with underscores. + # For example: DEPLOY_RS_SUDO_PASSWORD_MY_SYSTEM + # This defaults to `false` + environmentSudo = false; + # This is an optional list of arguments that will be passed to SSH. sshOpts = [ "-p" "2121" ]; diff --git a/interface.json b/interface.json index a96d1c2d..fd22fc82 100644 --- a/interface.json +++ b/interface.json @@ -38,6 +38,9 @@ }, "interactiveSudo": { "type": "boolean" + }, + "environmentSudo": { + "type": "boolean" } } }, diff --git a/src/cli.rs b/src/cli.rs index 43990f5c..ea632560 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -109,6 +109,9 @@ pub struct Opts { /// Prompt for sudo password during activation. #[arg(long)] interactive_sudo: Option, + /// Read sudo password from environment during activation. + #[arg(long)] + environment_sudo: Option, } /// Returns if the available Nix installation supports flakes @@ -563,6 +566,22 @@ async fn run_deploy( info!("You will now be prompted for the sudo password for {}.", node.node_settings.hostname); let sudo_password = rpassword::prompt_password(format!("(sudo for {}) Password: ", node.node_settings.hostname)).unwrap_or("".to_string()); + deploy_defs.sudo_password = Some(sudo_password); + } else if deploy_data.merged_settings.environment_sudo.unwrap_or(false) { + if deploy_data.merged_settings.sudo.is_some() { + warn!("Custom sudo commands should be configured to accept password input from stdin when using the 'interactive sudo' option. Deployment may fail if the custom command ignores stdin."); + } else { + // this configures sudo to hide the password prompt and accept input from stdin + // at the time of writing, deploy_defs.sudo defaults to 'sudo -u root' when using user=root and sshUser as non-root + let original = deploy_defs.sudo.unwrap_or("sudo".to_string()); + deploy_defs.sudo = Some(format!("{} -S -p \"\"", original)); + } + + let node_name_as_env = deploy_data.node_name.to_uppercase().replace("-", "_"); + + info!("Reading sudo password from environment variable DEPLOY_RS_SUDO_PASSWORD_{} for {}.", node_name_as_env, node.node_settings.hostname); + let sudo_password = std::env::var(format!("DEPLOY_RS_SUDO_PASSWORD_{}", node_name_as_env)).unwrap_or("".to_string()); + deploy_defs.sudo_password = Some(sudo_password); } @@ -710,7 +729,8 @@ pub async fn run(args: Option<&ArgMatches>) -> Result<(), RunError> { dry_activate: opts.dry_activate, remote_build: opts.remote_build, sudo: opts.sudo, - interactive_sudo: opts.interactive_sudo + interactive_sudo: opts.interactive_sudo, + environment_sudo: opts.environment_sudo }; let supports_flakes = test_flake_support().await.map_err(RunError::FlakeTest)?; diff --git a/src/data.rs b/src/data.rs index 12b0f01b..26dbbd2b 100644 --- a/src/data.rs +++ b/src/data.rs @@ -37,6 +37,8 @@ pub struct GenericSettings { pub remote_build: Option, #[serde(rename(deserialize = "interactiveSudo"))] pub interactive_sudo: Option, + #[serde(rename(deserialize = "environmentSudo"))] + pub environment_sudo: Option, } #[derive(Deserialize, Debug, Clone)] diff --git a/src/deploy.rs b/src/deploy.rs index fd535443..1293996d 100644 --- a/src/deploy.rs +++ b/src/deploy.rs @@ -301,7 +301,7 @@ pub async fn confirm_profile( .spawn() .map_err(ConfirmProfileError::SSHConfirm)?; - if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) { + if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) || deploy_data.merged_settings.environment_sudo.unwrap_or(false) { trace!("[confirm] Piping in sudo password"); handle_sudo_stdin(&mut ssh_confirm_child, deploy_defs) .await @@ -413,7 +413,7 @@ pub async fn deploy_profile( .spawn() .map_err(DeployProfileError::SSHSpawnActivate)?; - if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) { + if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) || deploy_data.merged_settings.environment_sudo.unwrap_or(false) { trace!("[activate] Piping in sudo password"); handle_sudo_stdin(&mut ssh_activate_child, deploy_defs) .await @@ -454,7 +454,7 @@ pub async fn deploy_profile( .spawn() .map_err(DeployProfileError::SSHSpawnActivate)?; - if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) { + if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) || deploy_data.merged_settings.environment_sudo.unwrap_or(false) { trace!("[activate] Piping in sudo password"); handle_sudo_stdin(&mut ssh_activate_child, deploy_defs) .await @@ -498,7 +498,7 @@ pub async fn deploy_profile( .spawn() .map_err(DeployProfileError::SSHWait)?; - if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) { + if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) || deploy_data.merged_settings.environment_sudo.unwrap_or(false) { trace!("[wait] Piping in sudo password"); handle_sudo_stdin(&mut ssh_wait_child, deploy_defs) .await @@ -581,7 +581,7 @@ pub async fn revoke( .spawn() .map_err(RevokeProfileError::SSHSpawnRevoke)?; - if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) { + if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) || deploy_data.merged_settings.environment_sudo.unwrap_or(false) { trace!("[revoke] Piping in sudo password"); handle_sudo_stdin(&mut ssh_revoke_child, deploy_defs) .await diff --git a/src/lib.rs b/src/lib.rs index 91ab7c76..9396fbaa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -166,6 +166,7 @@ pub struct CmdOverrides { pub activation_timeout: Option, pub sudo: Option, pub interactive_sudo: Option, + pub environment_sudo: Option, pub dry_activate: bool, pub remote_build: bool, } @@ -468,6 +469,9 @@ pub fn make_deploy_data<'a, 's>( if let Some(interactive_sudo) = cmd_overrides.interactive_sudo { merged_settings.interactive_sudo = Some(interactive_sudo); } + if let Some(environment_sudo) = cmd_overrides.environment_sudo { + merged_settings.environment_sudo = Some(environment_sudo); + } DeployData { node_name,