From 9e827209c1cf5ed93318babc2160914618e91d59 Mon Sep 17 00:00:00 2001 From: Andrew Dunn Date: Mon, 5 Oct 2026 12:00:54 -0400 Subject: [PATCH] install: Label root SSH drop-in by its full path atomic_replace_labeled looks up the label for its destination joined onto /, so it expects the directory it is given to be the root. The tmpfiles.d drop-in for --root-ssh-authorized-keys passed the /etc/tmpfiles.d directory instead, so the policy was asked about /bootc-root-ssh.conf. On Fedora and CentOS that is etc_runtime_t instead of etc_t, and restorecon wants to relabel the file. Write the drop-in relative to the root, as the other callers already do, and note the expectation on atomic_replace_labeled. Assisted-by: AI Closes: #2538 Signed-off-by: Andrew Dunn --- crates/lib/src/install/osconfig.rs | 52 ++++++++++++++++++++++++------ crates/lib/src/lsm.rs | 2 ++ 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/crates/lib/src/install/osconfig.rs b/crates/lib/src/install/osconfig.rs index 65e9370d9..c5117f603 100644 --- a/crates/lib/src/install/osconfig.rs +++ b/crates/lib/src/install/osconfig.rs @@ -41,16 +41,12 @@ pub(crate) fn inject_root_ssh_authorized_keys( format!("f~ /{root_path}/.ssh/authorized_keys 600 root root - {b64_encoded}\n"); crate::lsm::ensure_dir_labeled(dest, ETC_TMPFILES, None, 0o755.into(), sepolicy)?; - let tmpfiles_dir = dest.open_dir(ETC_TMPFILES)?; - crate::lsm::atomic_replace_labeled( - &tmpfiles_dir, - ROOT_SSH_TMPFILE, - 0o644.into(), - sepolicy, - |w| w.write_all(tmpfiles_content.as_bytes()).map_err(Into::into), - )?; - - println!("Injected: {ETC_TMPFILES}/{ROOT_SSH_TMPFILE}"); + let target = Utf8Path::new(ETC_TMPFILES).join(ROOT_SSH_TMPFILE); + crate::lsm::atomic_replace_labeled(dest, &target, 0o644.into(), sepolicy, |w| { + w.write_all(tmpfiles_content.as_bytes()).map_err(Into::into) + })?; + + println!("Injected: {target}"); Ok(()) } @@ -58,6 +54,42 @@ pub(crate) fn inject_root_ssh_authorized_keys( mod tests { use super::*; + #[test] + fn test_inject_root_ssh_label() -> Result<()> { + // Only meaningful where SELinux is enabled + if !crate::lsm::selinux_enabled() { + return Ok(()); + } + let host = Dir::open_ambient_dir("/", cap_std::ambient_authority())?; + let Some(policy) = crate::lsm::new_sepolicy_at(&host)? else { + return Ok(()); + }; + let root = &cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?; + + root.create_dir("etc")?; + root.create_dir("root")?; + inject_root_ssh_authorized_keys( + root, + root, + Some(&policy), + "ssh-ed25519 ABCDE example@demo\n", + )?; + + let expected = crate::lsm::require_label( + &policy, + Utf8Path::new("/etc/tmpfiles.d/bootc-root-ssh.conf"), + libc::S_IFREG | 0o644, + )?; + let f = root.open(format!("etc/tmpfiles.d/{ROOT_SSH_TMPFILE}"))?; + let mut buf = [0u8; 1024]; + let n = rustix::fs::fgetxattr(&f, "security.selinux", &mut buf)?; + assert_eq!( + std::str::from_utf8(&buf[..n])?.trim_end_matches('\0'), + expected.as_str() + ); + Ok(()) + } + #[test] fn test_inject_root_ssh_symlinked() -> Result<()> { let root = &cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?; diff --git a/crates/lib/src/lsm.rs b/crates/lib/src/lsm.rs index 565646314..772358f02 100644 --- a/crates/lib/src/lsm.rs +++ b/crates/lib/src/lsm.rs @@ -650,6 +650,8 @@ pub(crate) fn ensure_dir_labeled( } /// A wrapper for atomically writing a file, also optionally setting a SELinux label. +/// The label is looked up for `destname` as if `root` were `/`, so pass the +/// directory that stands for the target's root, never a subdirectory of it. pub(crate) fn atomic_replace_labeled( root: &Dir, destname: impl AsRef,