From 870da957821e7dcba487c898e306f905013ebd73 Mon Sep 17 00:00:00 2001 From: ckyrouac Date: Wed, 26 Feb 2025 12:43:08 -0500 Subject: [PATCH] reinstall: Enable ssh keys for all users Prior to this, the prompt to select users other that root would result in an error. Now, all ssh keys will be gathered into a single file and passed to bootc install to-existing-root --root-ssh-authorized-keys. Signed-off-by: ckyrouac --- Cargo.lock | 1 + system-reinstall-bootc/Cargo.toml | 1 + system-reinstall-bootc/src/main.rs | 14 ++-- system-reinstall-bootc/src/podman.rs | 17 ++--- system-reinstall-bootc/src/prompt.rs | 41 ++++++----- system-reinstall-bootc/src/users.rs | 101 +++++++++++++++++++-------- 6 files changed, 111 insertions(+), 64 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 00f55bee..220d36a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2185,6 +2185,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "tempfile", "tracing", "uzers", "which", diff --git a/system-reinstall-bootc/Cargo.toml b/system-reinstall-bootc/Cargo.toml index 183c76b0..4882a38f 100644 --- a/system-reinstall-bootc/Cargo.toml +++ b/system-reinstall-bootc/Cargo.toml @@ -24,6 +24,7 @@ rustix = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } serde_yaml = "0.9.22" +tempfile = "3.10.1" tracing = { workspace = true } uzers = "0.12.1" which = "7.0.2" diff --git a/system-reinstall-bootc/src/main.rs b/system-reinstall-bootc/src/main.rs index 86523c8b..f39d33f5 100644 --- a/system-reinstall-bootc/src/main.rs +++ b/system-reinstall-bootc/src/main.rs @@ -20,13 +20,17 @@ fn run() -> Result<()> { let config = config::ReinstallConfig::load().context("loading config")?; - let root_key = &prompt::get_root_key()?; + let ssh_key_file = tempfile::NamedTempFile::new()?; + let ssh_key_file_path = ssh_key_file + .path() + .to_str() + .ok_or_else(|| anyhow::anyhow!("unable to create authorized_key temp file"))?; - if root_key.is_none() { - return Ok(()); - } + tracing::trace!("ssh_key_file_path: {}", ssh_key_file_path); + + prompt::get_ssh_keys(ssh_key_file_path)?; - let mut reinstall_podman_command = podman::command(&config.bootc_image, root_key); + let mut reinstall_podman_command = podman::command(&config.bootc_image, ssh_key_file_path); println!(); diff --git a/system-reinstall-bootc/src/podman.rs b/system-reinstall-bootc/src/podman.rs index 5ba7250c..2cb44f6c 100644 --- a/system-reinstall-bootc/src/podman.rs +++ b/system-reinstall-bootc/src/podman.rs @@ -1,11 +1,10 @@ use super::ROOT_KEY_MOUNT_POINT; -use crate::users::UserKeys; use anyhow::{ensure, Context, Result}; use bootc_utils::CommandRunExt; use std::process::Command; use which::which; -pub(crate) fn command(image: &str, root_key: &Option) -> Command { +pub(crate) fn command(image: &str, ssh_key_file: &str) -> Command { let mut podman_command_and_args = [ // We use podman to run the bootc container. This might change in the future to remove the // podman dependency. @@ -44,17 +43,11 @@ pub(crate) fn command(image: &str, root_key: &Option) -> Command { .map(String::from) .to_vec(); - if let Some(root_key) = root_key.as_ref() { - let root_authorized_keys_path = root_key.authorized_keys_path.clone(); + podman_command_and_args.push("-v".to_string()); + podman_command_and_args.push(format!("{ssh_key_file}:{ROOT_KEY_MOUNT_POINT}")); - podman_command_and_args.push("-v".to_string()); - podman_command_and_args.push(format!( - "{root_authorized_keys_path}:{ROOT_KEY_MOUNT_POINT}" - )); - - bootc_command_and_args.push("--root-ssh-authorized-keys".to_string()); - bootc_command_and_args.push(ROOT_KEY_MOUNT_POINT.to_string()); - } + bootc_command_and_args.push("--root-ssh-authorized-keys".to_string()); + bootc_command_and_args.push(ROOT_KEY_MOUNT_POINT.to_string()); let all_args = [ podman_command_and_args, diff --git a/system-reinstall-bootc/src/prompt.rs b/system-reinstall-bootc/src/prompt.rs index 898ab4ab..53d2b460 100644 --- a/system-reinstall-bootc/src/prompt.rs +++ b/system-reinstall-bootc/src/prompt.rs @@ -1,7 +1,4 @@ -use crate::{ - prompt, - users::{get_all_users_keys, UserKeys}, -}; +use crate::{prompt, users::get_all_users_keys}; use anyhow::{ensure, Context, Result}; const NO_SSH_PROMPT: &str = "None of the users on this system found have authorized SSH keys, \ @@ -10,7 +7,9 @@ const NO_SSH_PROMPT: &str = "None of the users on this system found have authori fn prompt_single_user(user: &crate::users::UserKeys) -> Result> { let prompt = format!( - "Found only one user ({}) with {} SSH authorized keys. Would you like to import it and its keys to the system?", + "Found only one user ({}) with {} SSH authorized keys.\n\ + Would you like to import its SSH authorized keys\n\ + into the root user on the new bootc system?", user.user, user.num_keys(), ); @@ -25,7 +24,10 @@ fn prompt_user_selection( // TODO: Handle https://github.com/console-rs/dialoguer/issues/77 let selected_user_indices: Vec = dialoguer::MultiSelect::new() - .with_prompt("Select the users you want to install in the system (along with their authorized SSH keys)") + .with_prompt( + "Select which user's SSH authorized keys you want to\n\ + import into the root user of the new bootc system", + ) .items(&keys) .interact()?; @@ -62,10 +64,14 @@ pub(crate) fn ask_yes_no(prompt: &str, default: bool) -> Result { .context("prompting") } -/// For now we only support the root user. This function returns the root user's SSH -/// authorized_keys. In the future, when bootc supports multiple users, this function will need to -/// be updated to return the SSH authorized_keys for all the users selected by the user. -pub(crate) fn get_root_key() -> Result> { +/// Gather authorized keys for all user's of the host system +/// prompt the user to select which users's keys will be imported +/// into the target system's root user's authorized_keys file +/// +/// The keys are stored in a temporary file which is passed to +/// the podman run invocation to be used by +/// `bootc install to-existing-root --root-ssh-authorized-keys` +pub(crate) fn get_ssh_keys(temp_key_file_path: &str) -> Result<()> { let users = get_all_users_keys()?; if users.is_empty() { ensure!( @@ -73,7 +79,7 @@ pub(crate) fn get_root_key() -> Result> { "cancelled by user" ); - return Ok(None); + return Ok(()); } let selected_users = if users.len() == 1 { @@ -82,12 +88,13 @@ pub(crate) fn get_root_key() -> Result> { prompt_user_selection(&users)? }; - ensure!( - selected_users.iter().all(|x| x.user == "root"), - "Only importing the root user keys is supported for now" - ); + let keys = selected_users + .into_iter() + .map(|user_key| user_key.authorized_keys.as_str()) + .collect::>() + .join("\n"); - let root_key = selected_users.into_iter().find(|x| x.user == "root"); + std::fs::write(temp_key_file_path, keys.as_bytes())?; - Ok(root_key.cloned()) + Ok(()) } diff --git a/system-reinstall-bootc/src/users.rs b/system-reinstall-bootc/src/users.rs index 64225f4a..cd8ca2f8 100644 --- a/system-reinstall-bootc/src/users.rs +++ b/system-reinstall-bootc/src/users.rs @@ -1,5 +1,6 @@ use anyhow::{Context, Result}; use bootc_utils::CommandRunExt; +use bootc_utils::PathQuotedDisplay; use rustix::fs::Uid; use rustix::process::geteuid; use rustix::process::getuid; @@ -9,6 +10,7 @@ use std::collections::BTreeMap; use std::collections::BTreeSet; use std::fmt::Display; use std::fmt::Formatter; +use std::os::unix::process::CommandExt; use std::process::Command; use uzers::os::unix::UserExt; @@ -83,7 +85,6 @@ impl Drop for UidChange { pub(crate) struct UserKeys { pub(crate) user: String, pub(crate) authorized_keys: String, - pub(crate) authorized_keys_path: String, } impl UserKeys { @@ -134,51 +135,94 @@ impl<'a> SshdConfig<'a> { } } +fn get_keys_from_files(user: &uzers::User, keyfiles: &Vec<&str>) -> Result { + let home_dir = user.home_dir(); + let mut user_authorized_keys = String::new(); + + for keyfile in keyfiles { + let user_authorized_keys_path = home_dir.join(keyfile); + + if !user_authorized_keys_path.exists() { + tracing::debug!( + "Skipping authorized key file {} for user {} because it doesn't exist", + PathQuotedDisplay::new(&user_authorized_keys_path), + user.name().to_string_lossy() + ); + continue; + } + + // Safety: The UID should be valid because we got it from uzers + #[allow(unsafe_code)] + let user_uid = unsafe { Uid::from_raw(user.uid()) }; + + // Change the effective uid for this scope, to avoid accidentally reading files we + // shouldn't through symlinks + let _uid_change = UidChange::new(user_uid)?; + + let key = std::fs::read_to_string(&user_authorized_keys_path) + .context("Failed to read user's authorized keys")?; + user_authorized_keys.push_str(key.as_str()); + user_authorized_keys.push('\n'); + } + + Ok(user_authorized_keys) +} + +fn get_keys_from_command(command: &str, command_user: &str) -> Result { + let user_config = uzers::get_user_by_name(command_user).context(format!( + "authorized_keys_command_user {} not found", + command_user + ))?; + + let mut cmd = Command::new(command); + cmd.uid(user_config.uid()); + let output = cmd + .run_get_string() + .context(format!("running authorized_keys_command {}", command))?; + Ok(output) +} + pub(crate) fn get_all_users_keys() -> Result> { let loginctl_user_names = loginctl_users().context("enumerate users")?; let mut all_users_authorized_keys = Vec::new(); - let sshd_config = SshdConfig::parse()?; + let sshd_output = Command::new("sshd") + .arg("-T") + .run_get_string() + .context("running sshd -T")?; + tracing::trace!("sshd output:\n {}", sshd_output); + + let sshd_config = SshdConfig::parse(sshd_output.as_str())?; tracing::debug!("parsed sshd config: {:?}", sshd_config); for user_name in loginctl_user_names { let user_info = uzers::get_user_by_name(user_name.as_str()) .context(format!("user {} not found", user_name))?; - let home_dir = user_info.home_dir(); - let user_authorized_keys_path = home_dir.join(".ssh/authorized_keys"); - - if !user_authorized_keys_path.exists() { - tracing::debug!( - "Skipping user {} because it doesn't have an SSH authorized_keys file", - user_info.name().to_string_lossy() - ); - continue; + let mut user_authorized_keys = String::new(); + if !sshd_config.authorized_keys_files.is_empty() { + let keys = get_keys_from_files(&user_info, &sshd_config.authorized_keys_files)?; + user_authorized_keys.push_str(keys.as_str()); } + if sshd_config.authorized_keys_command != "none" { + let keys = get_keys_from_command( + &sshd_config.authorized_keys_command, + &sshd_config.authorized_keys_command_user, + )?; + user_authorized_keys.push_str(keys.as_str()); + }; + let user_name = user_info .name() .to_str() .context("user name is not valid utf-8")?; - let user_authorized_keys = { - // Safety: The UID should be valid because we got it from uzers - #[allow(unsafe_code)] - let user_uid = unsafe { Uid::from_raw(user_info.uid()) }; - - // Change the effective uid for this scope, to avoid accidentally reading files we - // shouldn't through symlinks - let _uid_change = UidChange::new(user_uid)?; - - std::fs::read_to_string(&user_authorized_keys_path) - .context("Failed to read user's authorized keys")? - }; - if user_authorized_keys.trim().is_empty() { tracing::debug!( - "Skipping user {} because it has an empty SSH authorized_keys file", - user_info.name().to_string_lossy() + "Skipping user {} because it has no SSH authorized_keys", + user_name ); continue; } @@ -186,12 +230,9 @@ pub(crate) fn get_all_users_keys() -> Result> { let user_keys = UserKeys { user: user_name.to_string(), authorized_keys: user_authorized_keys, - authorized_keys_path: user_authorized_keys_path - .to_str() - .context("user's authorized_keys path is not valid utf-8")? - .to_string(), }; + tracing::trace!("Found user keys: {:?}", user_keys); tracing::debug!( "Found user {} with {} SSH authorized_keys", user_keys.user,