Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

install: Add support for --root-ssh-authorized-keys #296

Merged
merged 2 commits into from
Feb 5, 2024
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
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,13 @@ jobs:
- name: Integration tests
run: |
set -xeuo pipefail
sudo podman run --rm -ti --privileged --env RUST_LOG=debug -v /:/target -v /var/lib/containers:/var/lib/containers -v ./usr/bin/bootc:/usr/bin/bootc --pid=host --security-opt label=disable \
echo 'ssh-ed25519 ABC0123 [email protected]' > test_authorized_keys
sudo podman run --rm -ti --privileged -v ./test_authorized_keys:/test_authorized_keys --env RUST_LOG=debug -v /:/target -v /var/lib/containers:/var/lib/containers -v ./usr/bin/bootc:/usr/bin/bootc --pid=host --security-opt label=disable \
quay.io/centos-bootc/fedora-bootc-dev:eln bootc install to-filesystem \
--karg=foo=bar --disable-selinux --replace=alongside /target
--karg=foo=bar --disable-selinux --replace=alongside --root-ssh-authorized-keys=/test_authorized_keys /target
ls -al /boot/loader/
sudo grep foo=bar /boot/loader/entries/*.conf
grep authorized_keys /ostree/deploy/default/deploy/*/etc/tmpfiles.d/bootc-root-ssh.conf
# TODO fix https://github.com/containers/bootc/pull/137
sudo chattr -i / /ostree/deploy/default/deploy/*
sudo rm /ostree/deploy/default -rf
Expand Down
34 changes: 30 additions & 4 deletions lib/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
// and filesystem setup.
pub(crate) mod baseline;
pub(crate) mod config;
pub(crate) mod osconfig;

use std::io::BufWriter;
use std::io::Write;
Expand Down Expand Up @@ -132,6 +133,16 @@ pub(crate) struct InstallConfigOpts {
/// Add a kernel argument
karg: Option<Vec<String>>,

/// The path to an `authorized_keys` that will be injected into the `root` account.
///
/// The implementation of this uses systemd `tmpfiles.d`, writing to a file named
/// `/etc/tmpfiles.d/bootc-root-ssh.conf`. This will have the effect that by default,
/// the SSH credentials will be set if not present. The intention behind this
/// is to allow mounting the whole `/root` home directory as a `tmpfs`, while still
/// getting the SSH key replaced on boot.
#[clap(long)]
root_ssh_authorized_keys: Option<Utf8PathBuf>,

/// Perform configuration changes suitable for a "generic" disk image.
/// At the moment:
///
Expand Down Expand Up @@ -261,6 +272,8 @@ pub(crate) struct State {
pub(crate) config_opts: InstallConfigOpts,
pub(crate) target_imgref: ostree_container::OstreeImageReference,
pub(crate) install_config: config::InstallConfiguration,
/// The parsed contents of the authorized_keys (not the file path)
pub(crate) root_ssh_authorized_keys: Option<String>,
}

impl State {
Expand Down Expand Up @@ -566,9 +579,9 @@ async fn initialize_ostree_root_from_self(
options.proxy_cfg = proxy_cfg;
println!("Creating initial deployment");
let target_image = state.target_imgref.to_string();
let state =
let imgstate =
ostree_container::deploy::deploy(&sysroot, stateroot, &src_imageref, Some(options)).await?;
let digest = state.manifest_digest.as_str();
let digest = imgstate.manifest_digest.as_str();
println!("Installed: {target_image}");
println!(" Digest: {digest}");

Expand Down Expand Up @@ -596,9 +609,13 @@ async fn initialize_ostree_root_from_self(
}
f.flush()?;

if let Some(contents) = state.root_ssh_authorized_keys.as_deref() {
osconfig::inject_root_ssh_authorized_keys(&root, contents)?;
}

let uname = rustix::system::uname();

let labels = crate::status::labels_of_config(&state.configuration);
let labels = crate::status::labels_of_config(&imgstate.configuration);
let timestamp = labels
.and_then(|l| {
l.get(oci_spec::image::ANNOTATION_CREATED)
Expand All @@ -607,7 +624,7 @@ async fn initialize_ostree_root_from_self(
.and_then(crate::status::try_deserialize_timestamp);
let aleph = InstallAleph {
image: src_imageref.imgref.name.clone(),
version: state.version().as_ref().map(|s| s.to_string()),
version: imgstate.version().as_ref().map(|s| s.to_string()),
timestamp,
kernel: uname.release().to_str()?.to_string(),
};
Expand Down Expand Up @@ -944,6 +961,14 @@ async fn prepare_install(
let install_config = config::load_config()?;
tracing::debug!("Loaded install configuration");

// Eagerly read the file now to ensure we error out early if e.g. it doesn't exist,
// instead of much later after we're 80% of the way through an install.
let root_ssh_authorized_keys = config_opts
.root_ssh_authorized_keys
.as_ref()
.map(|p| std::fs::read_to_string(p).with_context(|| format!("Reading {p}")))
.transpose()?;

// Create our global (read-only) state which gets wrapped in an Arc
// so we can pass it to worker threads too. Right now this just
// combines our command line options along with some bind mounts from the host.
Expand All @@ -954,6 +979,7 @@ async fn prepare_install(
config_opts,
target_imgref,
install_config,
root_ssh_authorized_keys,
});

Ok(state)
Expand Down
37 changes: 37 additions & 0 deletions lib/src/install/osconfig.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use anyhow::Result;
use camino::Utf8Path;
use cap_std::fs::Dir;
use cap_std_ext::{cap_std, dirext::CapStdExtDirExt};
use fn_error_context::context;

const ETC_TMPFILES: &str = "etc/tmpfiles.d";
const ROOT_SSH_TMPFILE: &str = "bootc-root-ssh.conf";

#[context("Injecting root authorized_keys")]
pub(crate) fn inject_root_ssh_authorized_keys(root: &Dir, contents: &str) -> Result<()> {
// While not documented right now, this one looks like it does not newline wrap
let b64_encoded = ostree_ext::glib::base64_encode(contents.as_bytes());
// See the example in https://systemd.io/CREDENTIALS/
let tmpfiles_content = format!("f~ /root/.ssh/authorized_keys 600 root root - {b64_encoded}\n");

let tmpfiles_dir = Utf8Path::new(ETC_TMPFILES);
root.create_dir_all(tmpfiles_dir)?;
let target = tmpfiles_dir.join(ROOT_SSH_TMPFILE);
root.atomic_write(&target, &tmpfiles_content)?;
println!("Injected: {target}");
Ok(())
}

#[test]
fn test_inject_root_ssh() -> Result<()> {
let root = &cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;

inject_root_ssh_authorized_keys(root, "ssh-ed25519 ABCDE example@demo\n").unwrap();

let content = root.read_to_string(format!("etc/tmpfiles.d/{ROOT_SSH_TMPFILE}"))?;
assert_eq!(
content,
"f~ /root/.ssh/authorized_keys 600 root root - c3NoLWVkMjU1MTkgQUJDREUgZXhhbXBsZUBkZW1vCg==\n"
);
Ok(())
}
Loading