Skip to content

Commit

Permalink
reinstall: Enable ssh keys for all users
Browse files Browse the repository at this point in the history
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 <ckyrouac@redhat.com>
  • Loading branch information
ckyrouac committed Feb 27, 2025
1 parent 3f5a43b commit 870da95
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 64 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions system-reinstall-bootc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
14 changes: 9 additions & 5 deletions system-reinstall-bootc/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!();

Expand Down
17 changes: 5 additions & 12 deletions system-reinstall-bootc/src/podman.rs
Original file line number Diff line number Diff line change
@@ -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<UserKeys>) -> 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.
Expand Down Expand Up @@ -44,17 +43,11 @@ pub(crate) fn command(image: &str, root_key: &Option<UserKeys>) -> 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,
Expand Down
41 changes: 24 additions & 17 deletions system-reinstall-bootc/src/prompt.rs
Original file line number Diff line number Diff line change
@@ -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, \
Expand All @@ -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<Vec<&crate::users::UserKeys>> {
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(),
);
Expand All @@ -25,7 +24,10 @@ fn prompt_user_selection(

// TODO: Handle https://github.com/console-rs/dialoguer/issues/77
let selected_user_indices: Vec<usize> = 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()?;

Expand Down Expand Up @@ -62,18 +64,22 @@ pub(crate) fn ask_yes_no(prompt: &str, default: bool) -> Result<bool> {
.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<Option<UserKeys>> {
/// 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!(
prompt::ask_yes_no(NO_SSH_PROMPT, false)?,
"cancelled by user"
);

return Ok(None);
return Ok(());
}

let selected_users = if users.len() == 1 {
Expand All @@ -82,12 +88,13 @@ pub(crate) fn get_root_key() -> Result<Option<UserKeys>> {
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::<Vec<&str>>()
.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(())
}
101 changes: 71 additions & 30 deletions system-reinstall-bootc/src/users.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -134,64 +135,104 @@ impl<'a> SshdConfig<'a> {
}
}

fn get_keys_from_files(user: &uzers::User, keyfiles: &Vec<&str>) -> Result<String> {
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<String> {
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<Vec<UserKeys>> {
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;
}

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,
Expand Down

0 comments on commit 870da95

Please sign in to comment.