bootc_lib/install/
osconfig.rs1use std::borrow::Cow;
2use std::io::Write;
3
4use anyhow::{Context, Result};
5use camino::{Utf8Path, Utf8PathBuf};
6use cap_std::fs::Dir;
7use cap_std_ext::{cap_std, dirext::CapStdExtDirExt};
8use fn_error_context::context;
9use ostree_ext::ostree;
10
11const ETC_TMPFILES: &str = "etc/tmpfiles.d";
12const ROOT_SSH_TMPFILE: &str = "bootc-root-ssh.conf";
13
14#[context("Injecting root authorized_keys")]
18pub(crate) fn inject_root_ssh_authorized_keys(
19 root: &Dir,
20 dest: &Dir,
21 sepolicy: Option<&ostree::SePolicy>,
22 contents: &str,
23) -> Result<()> {
24 let b64_encoded = ostree_ext::glib::base64_encode(contents.as_bytes());
26
27 let roothome_meta = root.symlink_metadata_optional("root")?;
30 let root_path = if roothome_meta.as_ref().filter(|m| m.is_symlink()).is_some() {
31 let path = root.read_link("root")?;
32 Utf8PathBuf::try_from(path)
33 .context("Reading /root symlink")
34 .map(Cow::Owned)?
35 } else {
36 Cow::Borrowed(Utf8Path::new("root"))
37 };
38
39 let tmpfiles_content =
41 format!("f~ /{root_path}/.ssh/authorized_keys 600 root root - {b64_encoded}\n");
42
43 crate::lsm::ensure_dir_labeled(dest, ETC_TMPFILES, None, 0o755.into(), sepolicy)?;
44 let target = Utf8Path::new(ETC_TMPFILES).join(ROOT_SSH_TMPFILE);
45 crate::lsm::atomic_replace_labeled(dest, &target, 0o644.into(), sepolicy, |w| {
46 w.write_all(tmpfiles_content.as_bytes()).map_err(Into::into)
47 })?;
48
49 println!("Injected: {target}");
50 Ok(())
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56
57 #[test]
58 fn test_inject_root_ssh_label() -> Result<()> {
59 if !crate::lsm::selinux_enabled() {
61 return Ok(());
62 }
63 let host = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
64 let Some(policy) = crate::lsm::new_sepolicy_at(&host)? else {
65 return Ok(());
66 };
67 let root = &cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
68
69 root.create_dir("etc")?;
70 root.create_dir("root")?;
71 inject_root_ssh_authorized_keys(
72 root,
73 root,
74 Some(&policy),
75 "ssh-ed25519 ABCDE example@demo\n",
76 )?;
77
78 let expected = crate::lsm::require_label(
79 &policy,
80 Utf8Path::new("/etc/tmpfiles.d/bootc-root-ssh.conf"),
81 libc::S_IFREG | 0o644,
82 )?;
83 let f = root.open(format!("etc/tmpfiles.d/{ROOT_SSH_TMPFILE}"))?;
84 let mut buf = [0u8; 1024];
85 let n = rustix::fs::fgetxattr(&f, "security.selinux", &mut buf)?;
86 assert_eq!(
87 std::str::from_utf8(&buf[..n])?.trim_end_matches('\0'),
88 expected.as_str()
89 );
90 Ok(())
91 }
92
93 #[test]
94 fn test_inject_root_ssh_symlinked() -> Result<()> {
95 let root = &cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
96
97 root.create_dir("etc")?;
99 root.symlink("var/roothome", "root")?;
101 inject_root_ssh_authorized_keys(root, root, None, "ssh-ed25519 ABCDE example@demo\n")
102 .unwrap();
103
104 let content = root.read_to_string(format!("etc/tmpfiles.d/{ROOT_SSH_TMPFILE}"))?;
105 assert_eq!(
106 content,
107 "f~ /var/roothome/.ssh/authorized_keys 600 root root - c3NoLWVkMjU1MTkgQUJDREUgZXhhbXBsZUBkZW1vCg==\n"
108 );
109
110 Ok(())
111 }
112
113 #[test]
114 fn test_inject_root_ssh_dir() -> Result<()> {
115 let root = &cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
116
117 root.create_dir("etc")?;
118 root.create_dir("root")?;
119 inject_root_ssh_authorized_keys(root, root, None, "ssh-ed25519 ABCDE example@demo\n")
120 .unwrap();
121
122 let content = root.read_to_string(format!("etc/tmpfiles.d/{ROOT_SSH_TMPFILE}"))?;
123 assert_eq!(
124 content,
125 "f~ /root/.ssh/authorized_keys 600 root root - c3NoLWVkMjU1MTkgQUJDREUgZXhhbXBsZUBkZW1vCg==\n"
126 );
127 Ok(())
128 }
129
130 #[test]
131 fn test_inject_root_ssh_separate_dest() -> Result<()> {
132 let root = &cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
133 let dest = &cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
134
135 root.symlink("var/roothome", "root")?;
136 dest.create_dir("etc")?;
137 inject_root_ssh_authorized_keys(root, dest, None, "ssh-ed25519 ABCDE example@demo\n")
138 .unwrap();
139
140 let content = dest.read_to_string(format!("etc/tmpfiles.d/{ROOT_SSH_TMPFILE}"))?;
141 assert_eq!(
142 content,
143 "f~ /var/roothome/.ssh/authorized_keys 600 root root - c3NoLWVkMjU1MTkgQUJDREUgZXhhbXBsZUBkZW1vCg==\n"
144 );
145 assert!(!root.exists("etc"));
146 Ok(())
147 }
148}