bootc_lib/install/
osconfig.rs

1use 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")]
15pub(crate) fn inject_root_ssh_authorized_keys(
16    root: &Dir,
17    sepolicy: Option<&ostree::SePolicy>,
18    contents: &str,
19) -> Result<()> {
20    // While not documented right now, this one looks like it does not newline wrap
21    let b64_encoded = ostree_ext::glib::base64_encode(contents.as_bytes());
22
23    // Eagerly resolve the path of /root in order to avoid tmpfiles.d clashes/problems.
24    // If it's local state (i.e. /root -> /var/roothome) then we resolve that symlink now.
25    let roothome_meta = root.symlink_metadata_optional("root")?;
26    let root_path = if roothome_meta.as_ref().filter(|m| m.is_symlink()).is_some() {
27        let path = root.read_link("root")?;
28        Utf8PathBuf::try_from(path)
29            .context("Reading /root symlink")
30            .map(Cow::Owned)?
31    } else {
32        Cow::Borrowed(Utf8Path::new("root"))
33    };
34
35    // See the example in https://systemd.io/CREDENTIALS/
36    let tmpfiles_content =
37        format!("f~ /{root_path}/.ssh/authorized_keys 600 root root - {b64_encoded}\n");
38
39    crate::lsm::ensure_dir_labeled(root, ETC_TMPFILES, None, 0o755.into(), sepolicy)?;
40    let tmpfiles_dir = root.open_dir(ETC_TMPFILES)?;
41    crate::lsm::atomic_replace_labeled(
42        &tmpfiles_dir,
43        ROOT_SSH_TMPFILE,
44        0o644.into(),
45        sepolicy,
46        |w| w.write_all(tmpfiles_content.as_bytes()).map_err(Into::into),
47    )?;
48
49    println!("Injected: {ETC_TMPFILES}/{ROOT_SSH_TMPFILE}");
50    Ok(())
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    #[test]
58    fn test_inject_root_ssh_symlinked() -> Result<()> {
59        let root = &cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
60
61        // The code expects this to exist, reasonably so
62        root.create_dir("etc")?;
63        // Test with a symlink
64        root.symlink("var/roothome", "root")?;
65        inject_root_ssh_authorized_keys(root, None, "ssh-ed25519 ABCDE example@demo\n").unwrap();
66
67        let content = root.read_to_string(format!("etc/tmpfiles.d/{ROOT_SSH_TMPFILE}"))?;
68        assert_eq!(
69            content,
70            "f~ /var/roothome/.ssh/authorized_keys 600 root root - c3NoLWVkMjU1MTkgQUJDREUgZXhhbXBsZUBkZW1vCg==\n"
71        );
72
73        Ok(())
74    }
75
76    #[test]
77    fn test_inject_root_ssh_dir() -> Result<()> {
78        let root = &cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
79
80        root.create_dir("etc")?;
81        root.create_dir("root")?;
82        inject_root_ssh_authorized_keys(root, None, "ssh-ed25519 ABCDE example@demo\n").unwrap();
83
84        let content = root.read_to_string(format!("etc/tmpfiles.d/{ROOT_SSH_TMPFILE}"))?;
85        assert_eq!(
86            content,
87            "f~ /root/.ssh/authorized_keys 600 root root - c3NoLWVkMjU1MTkgQUJDREUgZXhhbXBsZUBkZW1vCg==\n"
88        );
89        Ok(())
90    }
91}