Skip to content

Commit e477e4f

Browse files
committed
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 <[email protected]>
1 parent fe1d470 commit e477e4f

File tree

3 files changed

+67
-2
lines changed

3 files changed

+67
-2
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,13 @@ jobs:
132132
- name: Integration tests
133133
run: |
134134
set -xeuo pipefail
135-
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 \
135+
echo 'ssh-ed25519 ABC0123 [email protected]' > test_authorized_keys
136+
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 \
136137
quay.io/centos-bootc/fedora-bootc-dev:eln bootc install to-filesystem \
137-
--karg=foo=bar --disable-selinux --replace=alongside /target
138+
--karg=foo=bar --disable-selinux --replace=alongside --root-ssh-authorized-keys=/test_authorized_keys /target
138139
ls -al /boot/loader/
139140
sudo grep foo=bar /boot/loader/entries/*.conf
141+
grep authorized_keys /ostree/deploy/default/deploy/*/etc/tmpfiles.d/bootc-root-ssh.conf
140142
# TODO fix https://github.com/containers/bootc/pull/137
141143
sudo chattr -i / /ostree/deploy/default/deploy/*
142144
sudo rm /ostree/deploy/default -rf

lib/src/install.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
// and filesystem setup.
99
pub(crate) mod baseline;
1010
pub(crate) mod config;
11+
pub(crate) mod osconfig;
1112

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

136+
/// The path to an `authorized_keys` that will be injected into the `root` account.
137+
///
138+
/// The implementation of this uses systemd `tmpfiles.d`, writing to a file named
139+
/// `/etc/tmpfiles.d/bootc-root-ssh.conf`. This will have the effect that by default,
140+
/// the SSH credentials will be set if not present. The intention behind this
141+
/// is to allow mounting the whole `/root` home directory as a `tmpfs`, while still
142+
/// getting the SSH key replaced on boot.
143+
#[clap(long)]
144+
root_ssh_authorized_keys: Option<Utf8PathBuf>,
145+
135146
/// Perform configuration changes suitable for a "generic" disk image.
136147
/// At the moment:
137148
///
@@ -261,6 +272,8 @@ pub(crate) struct State {
261272
pub(crate) config_opts: InstallConfigOpts,
262273
pub(crate) target_imgref: ostree_container::OstreeImageReference,
263274
pub(crate) install_config: config::InstallConfiguration,
275+
/// The parsed contents of the authorized_keys (not the file path)
276+
pub(crate) root_ssh_authorized_keys: Option<String>,
264277
}
265278

266279
impl State {
@@ -596,6 +609,10 @@ async fn initialize_ostree_root_from_self(
596609
}
597610
f.flush()?;
598611

612+
if let Some(contents) = state.root_ssh_authorized_keys.as_deref() {
613+
osconfig::inject_root_ssh_authorized_keys(&root, contents)?;
614+
}
615+
599616
let uname = rustix::system::uname();
600617

601618
let labels = crate::status::labels_of_config(&imgstate.configuration);
@@ -944,6 +961,14 @@ async fn prepare_install(
944961
let install_config = config::load_config()?;
945962
tracing::debug!("Loaded install configuration");
946963

964+
// Eagerly read the file now to ensure we error out early if e.g. it doesn't exist,
965+
// instead of much later after we're 80% of the way through an install.
966+
let root_ssh_authorized_keys = config_opts
967+
.root_ssh_authorized_keys
968+
.as_ref()
969+
.map(|p| std::fs::read_to_string(p).with_context(|| format!("Reading {p}")))
970+
.transpose()?;
971+
947972
// Create our global (read-only) state which gets wrapped in an Arc
948973
// so we can pass it to worker threads too. Right now this just
949974
// combines our command line options along with some bind mounts from the host.
@@ -954,6 +979,7 @@ async fn prepare_install(
954979
config_opts,
955980
target_imgref,
956981
install_config,
982+
root_ssh_authorized_keys,
957983
});
958984

959985
Ok(state)

lib/src/install/osconfig.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
use anyhow::Result;
2+
use camino::Utf8Path;
3+
use cap_std::fs::Dir;
4+
use cap_std_ext::{cap_std, dirext::CapStdExtDirExt};
5+
use fn_error_context::context;
6+
7+
const ETC_TMPFILES: &str = "etc/tmpfiles.d";
8+
const ROOT_SSH_TMPFILE: &str = "bootc-root-ssh.conf";
9+
10+
#[context("Injecting root authorized_keys")]
11+
pub(crate) fn inject_root_ssh_authorized_keys(root: &Dir, contents: &str) -> Result<()> {
12+
// While not documented right now, this one looks like it does not newline wrap
13+
let b64_encoded = ostree_ext::glib::base64_encode(contents.as_bytes());
14+
// See the example in https://systemd.io/CREDENTIALS/
15+
let tmpfiles_content = format!("f~ /root/.ssh/authorized_keys 600 root root - {b64_encoded}\n");
16+
17+
let tmpfiles_dir = Utf8Path::new(ETC_TMPFILES);
18+
root.create_dir_all(tmpfiles_dir)?;
19+
let target = tmpfiles_dir.join(ROOT_SSH_TMPFILE);
20+
root.atomic_write(&target, &tmpfiles_content)?;
21+
println!("Injected: {target}");
22+
Ok(())
23+
}
24+
25+
#[test]
26+
fn test_inject_root_ssh() -> Result<()> {
27+
let root = &cap_std_ext::cap_tempfile::TempDir::new(cap_std::ambient_authority())?;
28+
29+
inject_root_ssh_authorized_keys(root, "ssh-ed25519 ABCDE example@demo\n").unwrap();
30+
31+
let content = root.read_to_string(format!("etc/tmpfiles.d/{ROOT_SSH_TMPFILE}"))?;
32+
assert_eq!(
33+
content,
34+
"f~ /root/.ssh/authorized_keys 600 root root - c3NoLWVkMjU1MTkgQUJDREUgZXhhbXBsZUBkZW1vCg==\n"
35+
);
36+
Ok(())
37+
}

0 commit comments

Comments
 (0)