aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRoman Melnikov2024-02-16 14:49:44 +0100
committerGitHub2024-02-16 14:49:44 +0100
commit0a0187794ac7f7a1e62cda3dabf8dc041f868790 (patch)
tree57e0fed6d50a149c6d5c808f671495191977d80d
parent1776009f1f3fb2b5d236b84d9815f2edee463a9b (diff)
parent5f694ef481610e8c4c77bb963b49e2d3b0d4db3c (diff)
Merge pull request #257 from n-hass/feature/interactive-sudo
Add support for entering sudo password interactively
-rw-r--r--Cargo.lock23
-rw-r--r--Cargo.toml3
-rw-r--r--examples/system/flake.nix1
-rw-r--r--interface.json3
-rw-r--r--src/cli.rs24
-rw-r--r--src/data.rs2
-rw-r--r--src/deploy.rs113
-rw-r--r--src/lib.rs6
8 files changed, 155 insertions, 20 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 55e5564..3bd43c3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -139,6 +139,7 @@ dependencies = [
"merge",
"notify",
"rnix",
+ "rpassword",
"serde",
"serde_json",
"signal-hook",
@@ -667,6 +668,27 @@ dependencies = [
]
[[package]]
+name = "rpassword"
+version = "7.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f"
+dependencies = [
+ "libc",
+ "rtoolbox",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "rtoolbox"
+version = "0.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e"
+dependencies = [
+ "libc",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -859,6 +881,7 @@ dependencies = [
"autocfg",
"bytes",
"libc",
+ "memchr",
"mio 0.7.6",
"num_cpus",
"once_cell",
diff --git a/Cargo.toml b/Cargo.toml
index 638e8a2..d884771 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -24,7 +24,7 @@ serde = { version = "1.0.104", features = [ "derive" ] }
serde_json = "1.0.48"
signal-hook = "0.3"
thiserror = "1.0"
-tokio = { version = "1.9.0", features = [ "process", "macros", "sync", "rt-multi-thread", "fs", "time" ] }
+tokio = { version = "1.9.0", features = [ "process", "macros", "sync", "rt-multi-thread", "fs", "time", "io-util" ] }
toml = "0.5"
whoami = "0.9.0"
yn = "0.1"
@@ -33,6 +33,7 @@ yn = "0.1"
# 1.45.2 (shipped in nixos-20.09); it requires rustc 1.46.0. See
# <https://github.com/serokell/deploy-rs/issues/27>:
smol_str = "=0.1.16"
+rpassword = "7.3.1"
[lib]
diff --git a/examples/system/flake.nix b/examples/system/flake.nix
index bcc841c..d8a19bf 100644
--- a/examples/system/flake.nix
+++ b/examples/system/flake.nix
@@ -26,6 +26,7 @@
sshOpts = [ "-p" "2221" ];
hostname = "localhost";
fastConnection = true;
+ interactiveSudo = true;
profiles = {
system = {
sshUser = "admin";
diff --git a/interface.json b/interface.json
index c733f25..a96d1c2 100644
--- a/interface.json
+++ b/interface.json
@@ -35,6 +35,9 @@
},
"tempPath": {
"type": "string"
+ },
+ "interactiveSudo": {
+ "type": "boolean"
}
}
},
diff --git a/src/cli.rs b/src/cli.rs
index 8ac6f59..47dc936 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -103,6 +103,9 @@ pub struct Opts {
/// Which sudo command to use. Must accept at least two arguments: user name to execute commands as and the rest is the command to execute
#[clap(long)]
sudo: Option<String>,
+ /// Prompt for sudo password during activation.
+ #[clap(long)]
+ interactive_sudo: Option<bool>,
}
/// Returns if the available Nix installation supports flakes
@@ -538,7 +541,25 @@ async fn run_deploy(
log_dir.as_deref(),
);
- let deploy_defs = deploy_data.defs()?;
+ let mut deploy_defs = deploy_data.defs()?;
+
+ if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) {
+ warn!("Interactive sudo is enabled! Using a sudo password is less secure than correctly configured SSH keys.\nPlease use keys in production environments.");
+
+ 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));
+ }
+
+ 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);
+ }
parts.push((deploy_flake, deploy_data, deploy_defs));
}
@@ -665,6 +686,7 @@ 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
};
let supports_flakes = test_flake_support().await.map_err(RunError::FlakeTest)?;
diff --git a/src/data.rs b/src/data.rs
index c507a31..12b0f01 100644
--- a/src/data.rs
+++ b/src/data.rs
@@ -35,6 +35,8 @@ pub struct GenericSettings {
pub sudo: Option<String>,
#[serde(default,rename(deserialize = "remoteBuild"))]
pub remote_build: Option<bool>,
+ #[serde(rename(deserialize = "interactiveSudo"))]
+ pub interactive_sudo: Option<bool>,
}
#[derive(Deserialize, Debug, Clone)]
diff --git a/src/deploy.rs b/src/deploy.rs
index a371c18..9f79d64 100644
--- a/src/deploy.rs
+++ b/src/deploy.rs
@@ -4,12 +4,12 @@
//
// SPDX-License-Identifier: MPL-2.0
-use log::{debug, info};
+use log::{debug, info, trace};
use std::path::Path;
use thiserror::Error;
-use tokio::process::Command;
+use tokio::{io::AsyncWriteExt, process::Command};
-use crate::{DeployDataDefsError, ProfileInfo};
+use crate::{DeployDataDefsError, DeployDefs, ProfileInfo};
struct ActivateCommandData<'a> {
sudo: &'a Option<String>,
@@ -242,6 +242,23 @@ fn test_revoke_command_builder() {
);
}
+async fn handle_sudo_stdin(ssh_activate_child: &mut tokio::process::Child, deploy_defs: &DeployDefs) -> Result<(), std::io::Error> {
+ match ssh_activate_child.stdin.as_mut() {
+ Some(stdin) => {
+ let _ = stdin.write_all(format!("{}\n",deploy_defs.sudo_password.clone().unwrap_or("".to_string())).as_bytes()).await;
+ Ok(())
+ }
+ None => {
+ Err(
+ std::io::Error::new(
+ std::io::ErrorKind::Other,
+ "Failed to open stdin for sudo command",
+ )
+ )
+ }
+ }
+}
+
#[derive(Error, Debug)]
pub enum ConfirmProfileError {
#[error("Failed to run confirmation command over SSH (the server should roll back): {0}")]
@@ -259,7 +276,9 @@ pub async fn confirm_profile(
ssh_addr: &str,
) -> Result<(), ConfirmProfileError> {
let mut ssh_confirm_command = Command::new("ssh");
- ssh_confirm_command.arg(ssh_addr);
+ ssh_confirm_command
+ .arg(ssh_addr)
+ .stdin(std::process::Stdio::piped());
for ssh_opt in &deploy_data.merged_settings.ssh_opts {
ssh_confirm_command.arg(ssh_opt);
@@ -277,11 +296,22 @@ pub async fn confirm_profile(
confirm_command
);
- let ssh_confirm_exit_status = ssh_confirm_command
+ let mut ssh_confirm_child = ssh_confirm_command
.arg(confirm_command)
- .status()
- .await
+ .spawn()
.map_err(ConfirmProfileError::SSHConfirm)?;
+
+ if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) {
+ trace!("[confirm] Piping in sudo password");
+ handle_sudo_stdin(&mut ssh_confirm_child, deploy_defs)
+ .await
+ .map_err(ConfirmProfileError::SSHConfirm)?;
+ }
+
+ let ssh_confirm_exit_status = ssh_confirm_child
+ .wait()
+ .await
+ .map_err(ConfirmProfileError::SSHConfirm)?;
match ssh_confirm_exit_status.code() {
Some(0) => (),
@@ -308,6 +338,9 @@ pub enum DeployProfileError {
#[error("Waiting over SSH resulted in a bad exit code: {0:?}")]
SSHWaitExit(Option<i32>),
+ #[error("Failed to pipe to child stdin: {0}")]
+ SSHActivatePipe(std::io::Error),
+
#[error("Error confirming deployment: {0}")]
Confirm(#[from] ConfirmProfileError),
#[error("Deployment data invalid: {0}")]
@@ -364,16 +397,29 @@ pub async fn deploy_profile(
let ssh_addr = format!("{}@{}", deploy_defs.ssh_user, hostname);
let mut ssh_activate_command = Command::new("ssh");
- ssh_activate_command.arg(&ssh_addr);
+ ssh_activate_command
+ .arg(&ssh_addr)
+ .stdin(std::process::Stdio::piped());
for ssh_opt in &deploy_data.merged_settings.ssh_opts {
ssh_activate_command.arg(&ssh_opt);
}
if !magic_rollback || dry_activate || boot {
- let ssh_activate_exit_status = ssh_activate_command
+ let mut ssh_activate_child = ssh_activate_command
.arg(self_activate_command)
- .status()
+ .spawn()
+ .map_err(DeployProfileError::SSHSpawnActivate)?;
+
+ if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) {
+ trace!("[activate] Piping in sudo password");
+ handle_sudo_stdin(&mut ssh_activate_child, deploy_defs)
+ .await
+ .map_err(DeployProfileError::SSHActivatePipe)?;
+ }
+
+ let ssh_activate_exit_status = ssh_activate_child
+ .wait()
.await
.map_err(DeployProfileError::SSHActivate)?;
@@ -401,16 +447,25 @@ pub async fn deploy_profile(
debug!("Constructed wait command: {}", self_wait_command);
- let ssh_activate = ssh_activate_command
+ let mut ssh_activate_child = ssh_activate_command
.arg(self_activate_command)
.spawn()
.map_err(DeployProfileError::SSHSpawnActivate)?;
+ if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) {
+ trace!("[activate] Piping in sudo password");
+ handle_sudo_stdin(&mut ssh_activate_child, deploy_defs)
+ .await
+ .map_err(DeployProfileError::SSHActivatePipe)?;
+ }
+
info!("Creating activation waiter");
let mut ssh_wait_command = Command::new("ssh");
- ssh_wait_command.arg(&ssh_addr);
-
+ ssh_wait_command
+ .arg(&ssh_addr)
+ .stdin(std::process::Stdio::piped());
+
for ssh_opt in &deploy_data.merged_settings.ssh_opts {
ssh_wait_command.arg(ssh_opt);
}
@@ -419,7 +474,7 @@ pub async fn deploy_profile(
let (send_activated, recv_activated) = tokio::sync::oneshot::channel();
let thread = tokio::spawn(async move {
- let o = ssh_activate.wait_with_output().await;
+ let o = ssh_activate_child.wait_with_output().await;
let maybe_err = match o {
Err(x) => Some(DeployProfileError::SSHActivate(x)),
@@ -435,8 +490,21 @@ pub async fn deploy_profile(
send_activated.send(()).unwrap();
});
+
+ let mut ssh_wait_child = ssh_wait_command
+ .arg(self_wait_command)
+ .spawn()
+ .map_err(DeployProfileError::SSHWait)?;
+
+ if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) {
+ trace!("[wait] Piping in sudo password");
+ handle_sudo_stdin(&mut ssh_wait_child, deploy_defs)
+ .await
+ .map_err(DeployProfileError::SSHActivatePipe)?;
+ }
+
tokio::select! {
- x = ssh_wait_command.arg(self_wait_command).status() => {
+ x = ssh_wait_child.wait() => {
debug!("Wait command ended");
match x.map_err(DeployProfileError::SSHWait)?.code() {
Some(0) => (),
@@ -498,18 +566,27 @@ pub async fn revoke(
let ssh_addr = format!("{}@{}", deploy_defs.ssh_user, hostname);
let mut ssh_activate_command = Command::new("ssh");
- ssh_activate_command.arg(&ssh_addr);
+ ssh_activate_command
+ .arg(&ssh_addr)
+ .stdin(std::process::Stdio::piped());
for ssh_opt in &deploy_data.merged_settings.ssh_opts {
ssh_activate_command.arg(&ssh_opt);
}
- let ssh_revoke = ssh_activate_command
+ let mut ssh_revoke_child = ssh_activate_command
.arg(self_revoke_command)
.spawn()
.map_err(RevokeProfileError::SSHSpawnRevoke)?;
- let result = ssh_revoke.wait_with_output().await;
+ if deploy_data.merged_settings.interactive_sudo.unwrap_or(false) {
+ trace!("[revoke] Piping in sudo password");
+ handle_sudo_stdin(&mut ssh_revoke_child, deploy_defs)
+ .await
+ .map_err(RevokeProfileError::SSHRevoke)?;
+ }
+
+ let result = ssh_revoke_child.wait_with_output().await;
match result {
Err(x) => Err(RevokeProfileError::SSHRevoke(x)),
diff --git a/src/lib.rs b/src/lib.rs
index 663e26e..61fac6a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -165,6 +165,7 @@ pub struct CmdOverrides {
pub confirm_timeout: Option<u16>,
pub activation_timeout: Option<u16>,
pub sudo: Option<String>,
+ pub interactive_sudo: Option<bool>,
pub dry_activate: bool,
pub remote_build: bool,
}
@@ -334,6 +335,7 @@ pub struct DeployDefs {
pub ssh_user: String,
pub profile_user: String,
pub sudo: Option<String>,
+ pub sudo_password: Option<String>,
}
enum ProfileInfo {
ProfilePath {
@@ -369,6 +371,7 @@ impl<'a> DeployData<'a> {
ssh_user,
profile_user,
sudo,
+ sudo_password: None,
})
}
@@ -448,6 +451,9 @@ pub fn make_deploy_data<'a, 's>(
if let Some(activation_timeout) = cmd_overrides.activation_timeout {
merged_settings.activation_timeout = Some(activation_timeout);
}
+ if let Some(interactive_sudo) = cmd_overrides.interactive_sudo {
+ merged_settings.interactive_sudo = Some(interactive_sudo);
+ }
DeployData {
node_name,