Skip to content

Commit

Permalink
Merge pull request #296 from cgwalters/install-inject-ssh
Browse files Browse the repository at this point in the history
install: Add support for `--root-ssh-authorized-keys`
  • Loading branch information
cgwalters authored Feb 5, 2024
2 parents dbaf4ee + e477e4f commit 56811f4
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 6 deletions.
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(())
}

0 comments on commit 56811f4

Please sign in to comment.