Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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.30.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
85 changes: 60 additions & 25 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,24 +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().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 @@ -461,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 @@ -473,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 @@ -500,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 @@ -512,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 @@ -526,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 @@ -538,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
Loading