Skip to content

Commit ae33afb

Browse files
committed
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 <ckyrouac@redhat.com>
1 parent 3f5a43b commit ae33afb

File tree

6 files changed

+107
-64
lines changed

6 files changed

+107
-64
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

system-reinstall-bootc/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ rustix = { workspace = true }
2424
serde = { workspace = true, features = ["derive"] }
2525
serde_json = { workspace = true }
2626
serde_yaml = "0.9.22"
27+
tempfile = "3.10.1"
2728
tracing = { workspace = true }
2829
uzers = "0.12.1"
2930
which = "7.0.2"

system-reinstall-bootc/src/main.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ fn run() -> Result<()> {
2020

2121
let config = config::ReinstallConfig::load().context("loading config")?;
2222

23-
let root_key = &prompt::get_root_key()?;
23+
let ssh_key_file = tempfile::NamedTempFile::new()?;
24+
let ssh_key_file_path = ssh_key_file
25+
.path()
26+
.to_str()
27+
.ok_or_else(|| anyhow::anyhow!("unable to create authorized_key temp file"))?;
2428

25-
if root_key.is_none() {
26-
return Ok(());
27-
}
29+
prompt::get_ssh_keys(ssh_key_file_path)?;
2830

29-
let mut reinstall_podman_command = podman::command(&config.bootc_image, root_key);
31+
let mut reinstall_podman_command = podman::command(&config.bootc_image, ssh_key_file_path);
3032

3133
println!();
3234

system-reinstall-bootc/src/podman.rs

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
use super::ROOT_KEY_MOUNT_POINT;
2-
use crate::users::UserKeys;
32
use anyhow::{ensure, Context, Result};
43
use bootc_utils::CommandRunExt;
54
use std::process::Command;
65
use which::which;
76

8-
pub(crate) fn command(image: &str, root_key: &Option<UserKeys>) -> Command {
7+
pub(crate) fn command(image: &str, ssh_key_file: &str) -> Command {
98
let mut podman_command_and_args = [
109
// We use podman to run the bootc container. This might change in the future to remove the
1110
// podman dependency.
@@ -44,17 +43,11 @@ pub(crate) fn command(image: &str, root_key: &Option<UserKeys>) -> Command {
4443
.map(String::from)
4544
.to_vec();
4645

47-
if let Some(root_key) = root_key.as_ref() {
48-
let root_authorized_keys_path = root_key.authorized_keys_path.clone();
46+
podman_command_and_args.push("-v".to_string());
47+
podman_command_and_args.push(format!("{ssh_key_file}:{ROOT_KEY_MOUNT_POINT}"));
4948

50-
podman_command_and_args.push("-v".to_string());
51-
podman_command_and_args.push(format!(
52-
"{root_authorized_keys_path}:{ROOT_KEY_MOUNT_POINT}"
53-
));
54-
55-
bootc_command_and_args.push("--root-ssh-authorized-keys".to_string());
56-
bootc_command_and_args.push(ROOT_KEY_MOUNT_POINT.to_string());
57-
}
49+
bootc_command_and_args.push("--root-ssh-authorized-keys".to_string());
50+
bootc_command_and_args.push(ROOT_KEY_MOUNT_POINT.to_string());
5851

5952
let all_args = [
6053
podman_command_and_args,

system-reinstall-bootc/src/prompt.rs

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
use crate::{
2-
prompt,
3-
users::{get_all_users_keys, UserKeys},
4-
};
1+
use crate::{prompt, users::get_all_users_keys};
52
use anyhow::{ensure, Context, Result};
63

74
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
107

118
fn prompt_single_user(user: &crate::users::UserKeys) -> Result<Vec<&crate::users::UserKeys>> {
129
let prompt = format!(
13-
"Found only one user ({}) with {} SSH authorized keys. Would you like to import it and its keys to the system?",
10+
"Found only one user ({}) with {} SSH authorized keys.\n\
11+
Would you like to import its SSH authorized keys\n\
12+
into the root user on the new bootc system?",
1413
user.user,
1514
user.num_keys(),
1615
);
@@ -25,7 +24,10 @@ fn prompt_user_selection(
2524

2625
// TODO: Handle https://github.com/console-rs/dialoguer/issues/77
2726
let selected_user_indices: Vec<usize> = dialoguer::MultiSelect::new()
28-
.with_prompt("Select the users you want to install in the system (along with their authorized SSH keys)")
27+
.with_prompt(
28+
"Select which user's SSH authorized keys you want to\n\
29+
import into the root user of the new bootc system",
30+
)
2931
.items(&keys)
3032
.interact()?;
3133

@@ -62,18 +64,22 @@ pub(crate) fn ask_yes_no(prompt: &str, default: bool) -> Result<bool> {
6264
.context("prompting")
6365
}
6466

65-
/// For now we only support the root user. This function returns the root user's SSH
66-
/// authorized_keys. In the future, when bootc supports multiple users, this function will need to
67-
/// be updated to return the SSH authorized_keys for all the users selected by the user.
68-
pub(crate) fn get_root_key() -> Result<Option<UserKeys>> {
67+
/// Gather authorized keys for all user's of the host system
68+
/// prompt the user to select which users's keys will be imported
69+
/// into the target system's root user's authorized_keys file
70+
///
71+
/// The keys are stored in a temporary file which is passed to
72+
/// the podman run invocation to be used by
73+
/// `bootc install to-existing-root --root-ssh-authorized-keys`
74+
pub(crate) fn get_ssh_keys(temp_key_file_path: &str) -> Result<()> {
6975
let users = get_all_users_keys()?;
7076
if users.is_empty() {
7177
ensure!(
7278
prompt::ask_yes_no(NO_SSH_PROMPT, false)?,
7379
"cancelled by user"
7480
);
7581

76-
return Ok(None);
82+
return Ok(());
7783
}
7884

7985
let selected_users = if users.len() == 1 {
@@ -82,12 +88,13 @@ pub(crate) fn get_root_key() -> Result<Option<UserKeys>> {
8288
prompt_user_selection(&users)?
8389
};
8490

85-
ensure!(
86-
selected_users.iter().all(|x| x.user == "root"),
87-
"Only importing the root user keys is supported for now"
88-
);
91+
let keys = selected_users
92+
.into_iter()
93+
.map(|user_key| user_key.authorized_keys.as_str())
94+
.collect::<Vec<&str>>()
95+
.join("\n");
8996

90-
let root_key = selected_users.into_iter().find(|x| x.user == "root");
97+
std::fs::write(temp_key_file_path, keys.as_bytes())?;
9198

92-
Ok(root_key.cloned())
99+
Ok(())
93100
}

system-reinstall-bootc/src/users.rs

Lines changed: 69 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use std::collections::BTreeMap;
99
use std::collections::BTreeSet;
1010
use std::fmt::Display;
1111
use std::fmt::Formatter;
12+
use std::os::unix::process::CommandExt;
1213
use std::process::Command;
1314
use uzers::os::unix::UserExt;
1415

@@ -83,7 +84,6 @@ impl Drop for UidChange {
8384
pub(crate) struct UserKeys {
8485
pub(crate) user: String,
8586
pub(crate) authorized_keys: String,
86-
pub(crate) authorized_keys_path: String,
8787
}
8888

8989
impl UserKeys {
@@ -134,64 +134,103 @@ impl<'a> SshdConfig<'a> {
134134
}
135135
}
136136

137+
fn get_keys_from_files(user: &uzers::User, keyfiles: &Vec<&str>) -> Result<String> {
138+
let home_dir = user.home_dir();
139+
let mut user_authorized_keys = String::new();
140+
141+
for keyfile in keyfiles {
142+
let user_authorized_keys_path = home_dir.join(keyfile);
143+
144+
if !user_authorized_keys_path.exists() {
145+
tracing::debug!(
146+
"Skipping authorized key file {} for user {} because it doesn't exist",
147+
user_authorized_keys_path.to_string_lossy(),
148+
user.name().to_string_lossy()
149+
);
150+
continue;
151+
}
152+
153+
// Safety: The UID should be valid because we got it from uzers
154+
#[allow(unsafe_code)]
155+
let user_uid = unsafe { Uid::from_raw(user.uid()) };
156+
157+
// Change the effective uid for this scope, to avoid accidentally reading files we
158+
// shouldn't through symlinks
159+
let _uid_change = UidChange::new(user_uid)?;
160+
161+
let key = std::fs::read_to_string(&user_authorized_keys_path)
162+
.context("Failed to read user's authorized keys")?;
163+
user_authorized_keys.push_str(key.as_str());
164+
}
165+
166+
Ok(user_authorized_keys)
167+
}
168+
169+
fn get_keys_from_command(command: &str, command_user: &str) -> Result<String> {
170+
let user_config = uzers::get_user_by_name(command_user).context(format!(
171+
"authorized_keys_command_user {} not found",
172+
command_user
173+
))?;
174+
175+
let mut cmd = Command::new(command);
176+
cmd.uid(user_config.uid());
177+
let output = cmd
178+
.run_get_string()
179+
.context(format!("running authorized_keys_command {}", command))?;
180+
Ok(output)
181+
}
182+
137183
pub(crate) fn get_all_users_keys() -> Result<Vec<UserKeys>> {
138184
let loginctl_user_names = loginctl_users().context("enumerate users")?;
139185

140186
let mut all_users_authorized_keys = Vec::new();
141187

142-
let sshd_config = SshdConfig::parse()?;
188+
let sshd_output = Command::new("sshd")
189+
.arg("-T")
190+
.run_get_string()
191+
.context("running sshd -T")?;
192+
tracing::trace!("sshd output:\n {}", sshd_output);
193+
194+
let sshd_config = SshdConfig::parse(sshd_output.as_str())?;
143195
tracing::debug!("parsed sshd config: {:?}", sshd_config);
144196

145197
for user_name in loginctl_user_names {
146198
let user_info = uzers::get_user_by_name(user_name.as_str())
147199
.context(format!("user {} not found", user_name))?;
148200

149-
let home_dir = user_info.home_dir();
150-
let user_authorized_keys_path = home_dir.join(".ssh/authorized_keys");
151-
152-
if !user_authorized_keys_path.exists() {
153-
tracing::debug!(
154-
"Skipping user {} because it doesn't have an SSH authorized_keys file",
155-
user_info.name().to_string_lossy()
156-
);
157-
continue;
201+
let mut user_authorized_keys = String::new();
202+
if !sshd_config.authorized_keys_files.is_empty() {
203+
let keys = get_keys_from_files(&user_info, &sshd_config.authorized_keys_files)?;
204+
user_authorized_keys.push_str(keys.as_str());
158205
}
159206

207+
if sshd_config.authorized_keys_command != "none" {
208+
let keys = get_keys_from_command(
209+
&sshd_config.authorized_keys_command,
210+
&sshd_config.authorized_keys_command_user,
211+
)?;
212+
user_authorized_keys.push_str(keys.as_str());
213+
};
214+
160215
let user_name = user_info
161216
.name()
162217
.to_str()
163218
.context("user name is not valid utf-8")?;
164219

165-
let user_authorized_keys = {
166-
// Safety: The UID should be valid because we got it from uzers
167-
#[allow(unsafe_code)]
168-
let user_uid = unsafe { Uid::from_raw(user_info.uid()) };
169-
170-
// Change the effective uid for this scope, to avoid accidentally reading files we
171-
// shouldn't through symlinks
172-
let _uid_change = UidChange::new(user_uid)?;
173-
174-
std::fs::read_to_string(&user_authorized_keys_path)
175-
.context("Failed to read user's authorized keys")?
176-
};
177-
178220
if user_authorized_keys.trim().is_empty() {
179221
tracing::debug!(
180-
"Skipping user {} because it has an empty SSH authorized_keys file",
181-
user_info.name().to_string_lossy()
222+
"Skipping user {} because it has no SSH authorized_keys",
223+
user_name
182224
);
183225
continue;
184226
}
185227

186228
let user_keys = UserKeys {
187229
user: user_name.to_string(),
188230
authorized_keys: user_authorized_keys,
189-
authorized_keys_path: user_authorized_keys_path
190-
.to_str()
191-
.context("user's authorized_keys path is not valid utf-8")?
192-
.to_string(),
193231
};
194232

233+
tracing::trace!("Found user keys: {:?}", user_keys);
195234
tracing::debug!(
196235
"Found user {} with {} SSH authorized_keys",
197236
user_keys.user,

0 commit comments

Comments
 (0)