Skip to content

Commit 55303ae

Browse files
authored
Merge pull request #1146 from ckyrouac/reinstall-ssh
Smarter ssh authorized key search for system-reinstall-bootc
2 parents 73de2a8 + 870da95 commit 55303ae

File tree

6 files changed

+145
-63
lines changed

6 files changed

+145
-63
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: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,17 @@ 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+
tracing::trace!("ssh_key_file_path: {}", ssh_key_file_path);
30+
31+
prompt::get_ssh_keys(ssh_key_file_path)?;
2832

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

3135
println!();
3236

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: 105 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
use anyhow::{Context, Result};
22
use bootc_utils::CommandRunExt;
3+
use bootc_utils::PathQuotedDisplay;
34
use rustix::fs::Uid;
45
use rustix::process::geteuid;
56
use rustix::process::getuid;
67
use rustix::thread::set_thread_res_uid;
78
use serde_json::Value;
9+
use std::collections::BTreeMap;
810
use std::collections::BTreeSet;
911
use std::fmt::Display;
1012
use std::fmt::Formatter;
13+
use std::os::unix::process::CommandExt;
1114
use std::process::Command;
1215
use uzers::os::unix::UserExt;
1316

@@ -82,7 +85,6 @@ impl Drop for UidChange {
8285
pub(crate) struct UserKeys {
8386
pub(crate) user: String,
8487
pub(crate) authorized_keys: String,
85-
pub(crate) authorized_keys_path: String,
8688
}
8789

8890
impl UserKeys {
@@ -102,61 +104,135 @@ impl Display for UserKeys {
102104
}
103105
}
104106

107+
#[derive(Debug)]
108+
struct SshdConfig<'a> {
109+
authorized_keys_files: Vec<&'a str>,
110+
authorized_keys_command: &'a str,
111+
authorized_keys_command_user: &'a str,
112+
}
113+
114+
impl<'a> SshdConfig<'a> {
115+
pub fn parse(sshd_output: &'a str) -> Result<SshdConfig<'a>> {
116+
let config = sshd_output
117+
.lines()
118+
.filter_map(|line| line.split_once(' '))
119+
.collect::<BTreeMap<&str, &str>>();
120+
121+
let authorized_keys_files: Vec<&str> = config
122+
.get("authorizedkeysfile")
123+
.unwrap_or(&"none")
124+
.split_whitespace()
125+
.collect();
126+
let authorized_keys_command = config.get("authorizedkeyscommand").unwrap_or(&"none");
127+
let authorized_keys_command_user =
128+
config.get("authorizedkeyscommanduser").unwrap_or(&"none");
129+
130+
Ok(Self {
131+
authorized_keys_files,
132+
authorized_keys_command,
133+
authorized_keys_command_user,
134+
})
135+
}
136+
}
137+
138+
fn get_keys_from_files(user: &uzers::User, keyfiles: &Vec<&str>) -> Result<String> {
139+
let home_dir = user.home_dir();
140+
let mut user_authorized_keys = String::new();
141+
142+
for keyfile in keyfiles {
143+
let user_authorized_keys_path = home_dir.join(keyfile);
144+
145+
if !user_authorized_keys_path.exists() {
146+
tracing::debug!(
147+
"Skipping authorized key file {} for user {} because it doesn't exist",
148+
PathQuotedDisplay::new(&user_authorized_keys_path),
149+
user.name().to_string_lossy()
150+
);
151+
continue;
152+
}
153+
154+
// Safety: The UID should be valid because we got it from uzers
155+
#[allow(unsafe_code)]
156+
let user_uid = unsafe { Uid::from_raw(user.uid()) };
157+
158+
// Change the effective uid for this scope, to avoid accidentally reading files we
159+
// shouldn't through symlinks
160+
let _uid_change = UidChange::new(user_uid)?;
161+
162+
let key = std::fs::read_to_string(&user_authorized_keys_path)
163+
.context("Failed to read user's authorized keys")?;
164+
user_authorized_keys.push_str(key.as_str());
165+
user_authorized_keys.push('\n');
166+
}
167+
168+
Ok(user_authorized_keys)
169+
}
170+
171+
fn get_keys_from_command(command: &str, command_user: &str) -> Result<String> {
172+
let user_config = uzers::get_user_by_name(command_user).context(format!(
173+
"authorized_keys_command_user {} not found",
174+
command_user
175+
))?;
176+
177+
let mut cmd = Command::new(command);
178+
cmd.uid(user_config.uid());
179+
let output = cmd
180+
.run_get_string()
181+
.context(format!("running authorized_keys_command {}", command))?;
182+
Ok(output)
183+
}
184+
105185
pub(crate) fn get_all_users_keys() -> Result<Vec<UserKeys>> {
106186
let loginctl_user_names = loginctl_users().context("enumerate users")?;
107187

108188
let mut all_users_authorized_keys = Vec::new();
109189

190+
let sshd_output = Command::new("sshd")
191+
.arg("-T")
192+
.run_get_string()
193+
.context("running sshd -T")?;
194+
tracing::trace!("sshd output:\n {}", sshd_output);
195+
196+
let sshd_config = SshdConfig::parse(sshd_output.as_str())?;
197+
tracing::debug!("parsed sshd config: {:?}", sshd_config);
198+
110199
for user_name in loginctl_user_names {
111200
let user_info = uzers::get_user_by_name(user_name.as_str())
112201
.context(format!("user {} not found", user_name))?;
113202

114-
let home_dir = user_info.home_dir();
115-
let user_authorized_keys_path = home_dir.join(".ssh/authorized_keys");
116-
117-
if !user_authorized_keys_path.exists() {
118-
tracing::debug!(
119-
"Skipping user {} because it doesn't have an SSH authorized_keys file",
120-
user_info.name().to_string_lossy()
121-
);
122-
continue;
203+
let mut user_authorized_keys = String::new();
204+
if !sshd_config.authorized_keys_files.is_empty() {
205+
let keys = get_keys_from_files(&user_info, &sshd_config.authorized_keys_files)?;
206+
user_authorized_keys.push_str(keys.as_str());
123207
}
124208

209+
if sshd_config.authorized_keys_command != "none" {
210+
let keys = get_keys_from_command(
211+
&sshd_config.authorized_keys_command,
212+
&sshd_config.authorized_keys_command_user,
213+
)?;
214+
user_authorized_keys.push_str(keys.as_str());
215+
};
216+
125217
let user_name = user_info
126218
.name()
127219
.to_str()
128220
.context("user name is not valid utf-8")?;
129221

130-
let user_authorized_keys = {
131-
// Safety: The UID should be valid because we got it from uzers
132-
#[allow(unsafe_code)]
133-
let user_uid = unsafe { Uid::from_raw(user_info.uid()) };
134-
135-
// Change the effective uid for this scope, to avoid accidentally reading files we
136-
// shouldn't through symlinks
137-
let _uid_change = UidChange::new(user_uid)?;
138-
139-
std::fs::read_to_string(&user_authorized_keys_path)
140-
.context("Failed to read user's authorized keys")?
141-
};
142-
143222
if user_authorized_keys.trim().is_empty() {
144223
tracing::debug!(
145-
"Skipping user {} because it has an empty SSH authorized_keys file",
146-
user_info.name().to_string_lossy()
224+
"Skipping user {} because it has no SSH authorized_keys",
225+
user_name
147226
);
148227
continue;
149228
}
150229

151230
let user_keys = UserKeys {
152231
user: user_name.to_string(),
153232
authorized_keys: user_authorized_keys,
154-
authorized_keys_path: user_authorized_keys_path
155-
.to_str()
156-
.context("user's authorized_keys path is not valid utf-8")?
157-
.to_string(),
158233
};
159234

235+
tracing::trace!("Found user keys: {:?}", user_keys);
160236
tracing::debug!(
161237
"Found user {} with {} SSH authorized_keys",
162238
user_keys.user,

0 commit comments

Comments
 (0)