Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion libazureinit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ thiserror = "2.0.3"
tokio = { version = "1", features = ["full"] }
serde-xml-rs = "0.8.0"
serde_json = "1.0.96"
nix = {version = "0.31.1", features = ["fs", "user"]}
rustix = { version = "0.38", features = ["fs", "process"] }
users = "0.11"
block-utils = "0.11.1"
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
Expand Down
6 changes: 3 additions & 3 deletions libazureinit/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ pub enum Error {
},
#[error("failed to construct a C-style string")]
NulError(#[from] std::ffi::NulError),
#[error("nix call failed: {0}")]
Nix(#[from] nix::Error),
#[error("rustix call failed: {0}")]
Rustix(#[from] rustix::io::Errno),
#[error("The user {user} does not exist")]
UserMissing { user: String },
#[error("failed to get username from IMDS or local OVF files")]
Expand Down Expand Up @@ -106,7 +106,7 @@ impl Error {
Self::HttpStatus { .. } => "http status error",
Self::SubprocessFailed { .. } => "subprocess failed",
Self::NulError(_) => "C string nul byte",
Self::Nix(_) => "nix error",
Self::Rustix(_) => "rustix error",
Self::UserMissing { .. } => "user not found",
Self::UsernameFailure => "failed to determine username",
Self::InstanceMetadataFailure => {
Expand Down
7 changes: 4 additions & 3 deletions libazureinit/src/provision/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,14 @@ impl Provision {
#[instrument(skip_all)]
fn provision_ssh_keys(self) -> Result<(), Error> {
if !self.user.ssh_keys.is_empty() {
let user = nix::unistd::User::from_name(&self.user.name)?.ok_or(
let user = users::get_user_by_name(&self.user.name).ok_or(
Error::UserMissing {
user: self.user.name,
user: self.user.name.clone(),
},
)?;
let ssh_user = ssh::SshUser::from(&user);
ssh::provision_ssh(
&user,
&ssh_user,
&self.user.ssh_keys,
&self.config.ssh.authorized_keys_path,
self.config.ssh.query_sshd_config,
Expand Down
86 changes: 60 additions & 26 deletions libazureinit/src/provision/ssh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
use crate::error::Error;
use crate::imds::PublicKeys;
use lazy_static::lazy_static;
use nix::unistd::{chown, User};
use regex::Regex;
use rustix::fs::chown;
use rustix::process::{Gid, Uid};
use std::{
fs::{
OpenOptions, {File, Permissions},
Expand All @@ -22,6 +23,28 @@ use std::{
};
use tracing::{error, info, instrument};

/// User information needed for SSH provisioning.
///
/// This struct holds the minimal user information required to provision SSH keys:
/// the home directory path and ownership information (uid/gid) for setting
/// correct file permissions.
pub(crate) struct SshUser {
pub home_dir: PathBuf,
pub uid: u32,
pub gid: u32,
}

impl From<&users::User> for SshUser {
fn from(user: &users::User) -> Self {
use users::os::unix::UserExt;
SshUser {
home_dir: user.home_dir().to_path_buf(),
uid: user.uid(),
gid: user.primary_group_id(),
}
}
}

lazy_static! {
/// A regular expression to match the `PasswordAuthentication` setting in the SSH configuration.
static ref PASSWORD_REGEX: Regex = Regex::new(
Expand Down Expand Up @@ -53,11 +76,16 @@ lazy_static! {
/// or write to the `authorized_keys` file.
#[instrument(skip_all)]
pub(crate) fn provision_ssh(
user: &User,
user: &SshUser,
keys: &[PublicKeys],
authorized_keys_path: &Path,
query_sshd_config: bool,
) -> Result<(), Error> {
let home_dir = &user.home_dir;
// SAFETY: uid and gid values come from the users crate which returns valid system user IDs
let uid = unsafe { Uid::from_raw(user.uid) };
let gid = unsafe { Gid::from_raw(user.gid) };

let authorized_keys_path = if query_sshd_config {
tracing::info!(
"Attempting to get authorized keys path via sshd -G as configured."
Expand All @@ -66,24 +94,24 @@ pub(crate) fn provision_ssh(
match get_authorized_keys_path_from_sshd(|| {
Command::new("sshd").arg("-G").output()
}) {
Some(path) => user.dir.join(path),
Some(path) => home_dir.join(path),
None => {
tracing::warn!("sshd -G failed; using configured authorized_keys_path as fallback.");
user.dir.join(authorized_keys_path)
home_dir.join(authorized_keys_path)
}
}
} else {
user.dir.join(authorized_keys_path)
home_dir.join(authorized_keys_path)
};

let ssh_dir = user.dir.join(".ssh");
let ssh_dir = home_dir.join(".ssh");
std::fs::DirBuilder::new()
.recursive(true)
.mode(0o700)
.create(&ssh_dir)?;
std::fs::set_permissions(&ssh_dir, Permissions::from_mode(0o700))?;

chown(&ssh_dir, Some(user.uid), Some(user.gid))?;
chown(&ssh_dir, Some(uid), Some(gid))?;

tracing::info!(
target: "libazureinit::ssh::authorized_keys",
Expand All @@ -97,7 +125,7 @@ pub(crate) fn provision_ssh(
keys.iter()
.try_for_each(|key| writeln!(authorized_keys, "{}", key.key_data))?;

chown(&authorized_keys_path, Some(user.uid), Some(user.gid))?;
chown(&authorized_keys_path, Some(uid), Some(gid))?;

Ok(())
}
Expand Down Expand Up @@ -323,6 +351,8 @@ mod tests {
};
use tempfile::TempDir;

use super::SshUser;

fn create_output(status_code: i32, stdout: &str, stderr: &str) -> Output {
Output {
status: ExitStatus::from_raw(status_code),
Expand All @@ -331,25 +361,29 @@ mod tests {
}
}

fn get_test_user_with_home_dir(create_ssh_dir: bool) -> nix::unistd::User {
fn get_test_user_with_home_dir(
create_ssh_dir: bool,
) -> (SshUser, tempfile::TempDir) {
let home_dir =
tempfile::TempDir::new().expect("Failed to create temp directory");

let mut user = nix::unistd::User::from_name(
whoami::username().expect("Failed to get username").as_str(),
)
.expect("Failed to get user")
.expect("User does not exist");
user.dir = home_dir.path().into();
let current_user = users::get_current_uid();
let current_group = users::get_current_gid();

let user = SshUser {
home_dir: home_dir.path().to_path_buf(),
uid: current_user,
gid: current_group,
};

if create_ssh_dir {
std::fs::DirBuilder::new()
.mode(0o700)
.create(user.dir.join(".ssh"))
.create(user.home_dir.join(".ssh"))
.expect("Failed to create .ssh directory");
}

user
(user, home_dir)
}

#[test]
Expand Down Expand Up @@ -462,7 +496,7 @@ mod tests {
// chown without elevated permissions.
#[test]
fn test_provision_ssh() {
let user = get_test_user_with_home_dir(false);
let (user, _temp_dir) = get_test_user_with_home_dir(false);
let keys = vec![
PublicKeys {
key_data: "not-a-real-key abc123".to_string(),
Expand All @@ -474,11 +508,11 @@ mod tests {
},
];

let authorized_keys_path = user.dir.join(".ssh/xauthorized_keys");
let authorized_keys_path = user.home_dir.join(".ssh/xauthorized_keys");

provision_ssh(&user, &keys, &authorized_keys_path, false).unwrap();

let ssh_path = user.dir.join(".ssh");
let ssh_path = user.home_dir.join(".ssh");
let ssh_dir = std::fs::File::open(&ssh_path).unwrap();
let mut auth_file =
std::fs::File::open(&ssh_path.join("xauthorized_keys")).unwrap();
Expand All @@ -501,7 +535,7 @@ mod tests {
// /etc/skel includes it. This also checks that we fix the permissions if /etc/skel has been mis-configured.
#[test]
fn test_pre_existing_ssh_dir() {
let user = get_test_user_with_home_dir(true);
let (user, _temp_dir) = get_test_user_with_home_dir(true);
let keys = vec![
PublicKeys {
key_data: "not-a-real-key abc123".to_string(),
Expand All @@ -513,11 +547,11 @@ mod tests {
},
];

let authorized_keys_path = user.dir.join(".ssh/xauthorized_keys");
let authorized_keys_path = user.home_dir.join(".ssh/xauthorized_keys");

provision_ssh(&user, &keys, &authorized_keys_path, false).unwrap();

let ssh_dir = std::fs::File::open(user.dir.join(".ssh")).unwrap();
let ssh_dir = std::fs::File::open(user.home_dir.join(".ssh")).unwrap();
assert_eq!(
ssh_dir.metadata().unwrap().permissions(),
Permissions::from_mode(0o040700)
Expand All @@ -527,7 +561,7 @@ mod tests {
// Test that any pre-existing authorized_keys are overwritten.
#[test]
fn test_pre_existing_authorized_keys() {
let user = get_test_user_with_home_dir(true);
let (user, _temp_dir) = get_test_user_with_home_dir(true);
let keys = vec![
PublicKeys {
key_data: "not-a-real-key abc123".to_string(),
Expand All @@ -539,14 +573,14 @@ mod tests {
},
];

let authorized_keys_path = user.dir.join(".ssh/xauthorized_keys");
let authorized_keys_path = user.home_dir.join(".ssh/xauthorized_keys");

provision_ssh(&user, &keys[1..], &authorized_keys_path, false).unwrap();

provision_ssh(&user, &keys[1..], &authorized_keys_path, false).unwrap();

let mut auth_file =
std::fs::File::open(user.dir.join(".ssh/xauthorized_keys"))
std::fs::File::open(user.home_dir.join(".ssh/xauthorized_keys"))
.unwrap();
let mut buf = String::new();
auth_file.read_to_string(&mut buf).unwrap();
Expand Down