apply-local: Escalate privileges only during activation

Fixes #85.
This commit is contained in:
Zhaofeng Li 2022-06-03 23:51:32 -07:00
parent fa07814abf
commit ca12be27ed
6 changed files with 64 additions and 61 deletions

View file

@ -4,6 +4,7 @@
- `--reboot` is added to trigger a reboot and wait for the node to come back up. - `--reboot` is added to trigger a reboot and wait for the node to come back up.
- The target user is no longer explicitly set when `deployment.targetUser` is null ([#91](https://github.com/zhaofengli/colmena/pull/91)). - The target user is no longer explicitly set when `deployment.targetUser` is null ([#91](https://github.com/zhaofengli/colmena/pull/91)).
- In `apply-local`, we now only escalate privileges during activation ([#85](https://github.com/zhaofengli/colmena/issues/85)).
## [Release 0.3.0](https://github.com/zhaofengli/colmena/releases/tag/v0.3.0) (2022/04/27) ## [Release 0.3.0](https://github.com/zhaofengli/colmena/releases/tag/v0.3.0) (2022/04/27)

View file

@ -1,10 +1,8 @@
use std::env;
use regex::Regex; use regex::Regex;
use std::collections::HashMap; use std::collections::HashMap;
use clap::{Arg, Command as ClapCommand, ArgMatches}; use clap::{Arg, Command as ClapCommand, ArgMatches};
use tokio::fs; use tokio::fs;
use tokio::process::Command;
use crate::error::ColmenaError; use crate::error::ColmenaError;
use crate::nix::deployment::{ use crate::nix::deployment::{
@ -13,7 +11,7 @@ use crate::nix::deployment::{
TargetNode, TargetNode,
Options, Options,
}; };
use crate::nix::{NodeName, host}; use crate::nix::{NodeName, host::Local as LocalHost};
use crate::progress::SimpleProgressOutput; use crate::progress::SimpleProgressOutput;
use crate::util; use crate::util;
@ -29,12 +27,6 @@ pub fn subcommand() -> ClapCommand<'static> {
.arg(Arg::new("sudo") .arg(Arg::new("sudo")
.long("sudo") .long("sudo")
.help("Attempt to escalate privileges if not run as root")) .help("Attempt to escalate privileges if not run as root"))
.arg(Arg::new("sudo-command")
.long("sudo-command")
.value_name("COMMAND")
.help("Command to use to escalate privileges")
.default_value("sudo")
.takes_value(true))
.arg(Arg::new("verbose") .arg(Arg::new("verbose")
.short('v') .short('v')
.long("verbose") .long("verbose")
@ -53,13 +45,22 @@ By default, Colmena will deploy keys set in `deployment.keys` before activating
.long("node") .long("node")
.help("Override the node name to use") .help("Override the node name to use")
.takes_value(true)) .takes_value(true))
.arg(Arg::new("we-are-launched-by-sudo")
.long("we-are-launched-by-sudo") // Removed
.arg(Arg::new("sudo-command")
.long("sudo-command")
.value_name("COMMAND")
.help("Removed: Configure deployment.privilegeEscalationCommand in node configuration")
.hide(true) .hide(true)
.takes_value(false)) .takes_value(true))
} }
pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(), ColmenaError> { pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(), ColmenaError> {
if local_args.occurrences_of("sudo-command") > 0 {
log::error!("--sudo-command has been removed. Please configure it in deployment.privilegeEscalationCommand in the node configuration.");
quit::with_code(1);
}
// Sanity check: Are we running NixOS? // Sanity check: Are we running NixOS?
if let Ok(os_release) = fs::read_to_string("/etc/os-release").await { if let Ok(os_release) = fs::read_to_string("/etc/os-release").await {
let re = Regex::new(r#"ID="?nixos"?"#).unwrap(); let re = Regex::new(r#"ID="?nixos"?"#).unwrap();
@ -72,23 +73,14 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(
quit::with_code(5); quit::with_code(5);
} }
// Escalate privileges? let escalate_privileges = local_args.is_present("sudo");
let verbose = local_args.is_present("verbose") || escalate_privileges; // cannot use spinners with interactive sudo
{ {
let euid: u32 = unsafe { libc::geteuid() }; let euid: u32 = unsafe { libc::geteuid() };
if euid != 0 { if euid != 0 && !escalate_privileges {
if local_args.is_present("we-are-launched-by-sudo") { log::warn!("Colmena was not started by root. This is probably not going to work.");
log::error!("Failed to escalate privileges. We are still not root despite a successful sudo invocation."); log::warn!("Hint: Add the --sudo flag.");
quit::with_code(3);
}
if local_args.is_present("sudo") {
let sudo = local_args.value_of("sudo-command").unwrap();
escalate(sudo).await;
} else {
log::warn!("Colmena was not started by root. This is probably not going to work.");
log::warn!("Hint: Add the --sudo flag.");
}
} }
} }
@ -113,9 +105,15 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(
log::error!("Hint: Set deployment.allowLocalDeployment to true."); log::error!("Hint: Set deployment.allowLocalDeployment to true.");
quit::with_code(2); quit::with_code(2);
} }
let mut host = LocalHost::new(nix_options);
if escalate_privileges {
let command = info.privilege_escalation_command().to_owned();
host.set_privilege_escalation_command(Some(command));
}
TargetNode::new( TargetNode::new(
hostname.clone(), hostname.clone(),
Some(host::local(nix_options)), Some(host.upcast()),
info.clone(), info.clone(),
) )
} else { } else {
@ -127,7 +125,7 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(
let mut targets = HashMap::new(); let mut targets = HashMap::new();
targets.insert(hostname.clone(), target); targets.insert(hostname.clone(), target);
let mut output = SimpleProgressOutput::new(local_args.is_present("verbose")); let mut output = SimpleProgressOutput::new(verbose);
let progress = output.get_sender(); let progress = output.get_sender();
let mut deployment = Deployment::new(hive, targets, goal, progress); let mut deployment = Deployment::new(hive, targets, goal, progress);
@ -149,21 +147,3 @@ pub async fn run(_global_args: &ArgMatches, local_args: &ArgMatches) -> Result<(
Ok(()) Ok(())
} }
async fn escalate(sudo: &str) -> ! {
// Restart ourselves with sudo
let argv: Vec<String> = env::args().collect();
let exit = Command::new(sudo)
.arg("--")
.args(argv)
.arg("--we-are-launched-by-sudo")
.spawn()
.expect("Failed to run sudo to escalate privileges")
.wait()
.await
.expect("Failed to wait on child");
// Exit with the same exit code
quit::with_code(exit.code().unwrap());
}

View file

@ -35,6 +35,7 @@ use super::{
CopyDirection, CopyDirection,
CopyOptions, CopyOptions,
RebootOptions, RebootOptions,
host::Local as LocalHost,
key::{Key, UploadAt as UploadKeyAt}, key::{Key, UploadAt as UploadKeyAt},
evaluator::{ evaluator::{
DrvSetEvaluator, DrvSetEvaluator,
@ -42,7 +43,6 @@ use super::{
EvalError, EvalError,
}, },
}; };
use super::host;
/// A deployment. /// A deployment.
pub type DeploymentHandle = Arc<Deployment>; pub type DeploymentHandle = Arc<Deployment>;
@ -450,7 +450,7 @@ impl Deployment {
let arc_self = self.clone(); let arc_self = self.clone();
let profile: Profile = build_job.run(|job| async move { let profile: Profile = build_job.run(|job| async move {
// FIXME: Remote builder? // FIXME: Remote builder?
let mut builder = host::local(arc_self.nix_options.clone()); let mut builder = LocalHost::new(arc_self.nix_options.clone()).upcast();
builder.set_job(Some(job.clone())); builder.set_job(Some(job.clone()));
let profile = profile_drv.realize(&mut builder).await?; let profile = profile_drv.realize(&mut builder).await?;

View file

@ -19,6 +19,7 @@ use super::{CopyDirection, CopyOptions, Host, key_uploader};
pub struct Local { pub struct Local {
job: Option<JobHandle>, job: Option<JobHandle>,
nix_options: NixOptions, nix_options: NixOptions,
privilege_escalation_command: Option<Vec<String>>,
} }
impl Local { impl Local {
@ -26,6 +27,7 @@ impl Local {
Self { Self {
job: None, job: None,
nix_options, nix_options,
privilege_escalation_command: None,
} }
} }
} }
@ -71,17 +73,15 @@ impl Host for Local {
if goal.should_switch_profile() { if goal.should_switch_profile() {
let path = profile.as_path().to_str().unwrap(); let path = profile.as_path().to_str().unwrap();
Command::new("nix-env") self.make_privileged_command(&["nix-env", "--profile", SYSTEM_PROFILE, "--set", path])
.args(&["--profile", SYSTEM_PROFILE])
.args(&["--set", path])
.passthrough() .passthrough()
.await?; .await?;
} }
let activation_command = profile.activation_command(goal).unwrap(); let command = {
let mut command = Command::new(&activation_command[0]); let activation_command = profile.activation_command(goal).unwrap();
command self.make_privileged_command(&activation_command)
.args(&activation_command[1..]); };
let mut execution = CommandExecution::new(command); let mut execution = CommandExecution::new(command);
@ -126,6 +126,14 @@ impl Host for Local {
} }
impl Local { impl Local {
pub fn set_privilege_escalation_command(&mut self, command: Option<Vec<String>>) {
self.privilege_escalation_command = command;
}
pub fn upcast(self) -> Box<dyn Host> {
Box::new(self)
}
/// "Uploads" a single key. /// "Uploads" a single key.
async fn upload_key(&mut self, name: &str, key: &Key, require_ownership: bool) -> ColmenaResult<()> { async fn upload_key(&mut self, name: &str, key: &Key, require_ownership: bool) -> ColmenaResult<()> {
if let Some(job) = &self.job { if let Some(job) = &self.job {
@ -145,4 +153,20 @@ impl Local {
let uploader = command.spawn()?; let uploader = command.spawn()?;
key_uploader::feed_uploader(uploader, key, self.job.clone()).await key_uploader::feed_uploader(uploader, key, self.job.clone()).await
} }
/// Constructs a command with privilege escalation.
fn make_privileged_command<S: AsRef<str>>(&self, command: &[S]) -> Command {
let mut full_command = Vec::new();
if let Some(esc) = &self.privilege_escalation_command {
full_command.extend(esc.iter().map(|s| s.as_str()));
}
full_command.extend(command.iter().map(|s| s.as_ref()));
let mut result = Command::new(full_command[0]);
if full_command.len() > 1 {
result.args(&full_command[1..]);
}
result
}
} }

View file

@ -4,7 +4,7 @@ use async_trait::async_trait;
use crate::error::{ColmenaError, ColmenaResult}; use crate::error::{ColmenaError, ColmenaResult};
use crate::job::JobHandle; use crate::job::JobHandle;
use super::{StorePath, Profile, Goal, Key, NixOptions}; use super::{StorePath, Profile, Goal, Key};
mod ssh; mod ssh;
pub use ssh::Ssh; pub use ssh::Ssh;
@ -14,10 +14,6 @@ pub use local::Local;
mod key_uploader; mod key_uploader;
pub(crate) fn local(nix_options: NixOptions) -> Box<dyn Host + 'static> {
Box::new(Local::new(nix_options))
}
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub enum CopyDirection { pub enum CopyDirection {
ToRemote, ToRemote,

View file

@ -156,6 +156,8 @@ impl NodeConfig {
#[cfg_attr(not(target_os = "linux"), allow(dead_code))] #[cfg_attr(not(target_os = "linux"), allow(dead_code))]
pub fn allows_local_deployment(&self) -> bool { self.allow_local_deployment } pub fn allows_local_deployment(&self) -> bool { self.allow_local_deployment }
pub fn privilege_escalation_command(&self) -> &Vec<String> { &self.privilege_escalation_command }
pub fn build_on_target(&self) -> bool { self.build_on_target } pub fn build_on_target(&self) -> bool { self.build_on_target }
pub fn set_build_on_target(&mut self, enable: bool) { pub fn set_build_on_target(&mut self, enable: bool) {
self.build_on_target = enable; self.build_on_target = enable;