mirror of
https://github.com/containers/bootc.git
synced 2026-02-05 15:45:53 +01:00
install: Add support for --root-ssh-authorized-keys
The current `bootc install` model is VERY opinionated: we install the running container image to disk, and that is (almost!) it. The only non-container out of band state that we support injecting right now is kargs (via `--karg`) - we know we need this for machine local kernel arguments. (We do have a current outstanding PR to add a highly generic mechanism to inject arbitrary files in `/etc`, but I want to think about that more) THis current strict stance is quite painful for a use case like "take a generic container image and bootc install to-filesystem --alongside" in a cloud environment, because the generic container may not have cloud-init. With this change it becomes extremely convenient to: - Boot generic cloud image (e.g. AMI with apt/dnf + cloud-init) - cloud-init fetches SSH keys from hypervisor (instance metadata) - podman run -v /root/.ssh/authorized_keys:/keys:ro <image> bootc install ... --root-ssh-authorized-keys=/keys` And then the instance will carry forward those hypervisor-provided keys but without a dependency on cloud-init. Another use case for this of course is being the backend of things like Anaconda's kickstart verbs which support injecting SSH keys. Signed-off-by: Colin Walters <walters@verbum.org>
This commit is contained in:
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -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 testcase@example.com' > 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
|
||||
|
||||
@@ -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;
|
||||
@@ -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:
|
||||
///
|
||||
@@ -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 {
|
||||
@@ -596,6 +609,10 @@ 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(&imgstate.configuration);
|
||||
@@ -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.
|
||||
@@ -954,6 +979,7 @@ async fn prepare_install(
|
||||
config_opts,
|
||||
target_imgref,
|
||||
install_config,
|
||||
root_ssh_authorized_keys,
|
||||
});
|
||||
|
||||
Ok(state)
|
||||
|
||||
37
lib/src/install/osconfig.rs
Normal file
37
lib/src/install/osconfig.rs
Normal file
@@ -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(())
|
||||
}
|
||||
Reference in New Issue
Block a user