From 3fd409865495c642cd9698e6eae503d1ab382262 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Thu, 30 Oct 2025 09:38:06 -0400 Subject: [PATCH 1/2] Clean up systemd credentials handling Create a new credentials module that consolidates systemd credential injection functionality. Assisted-by: Claude Code (Sonnet 4.5) Signed-off-by: Colin Walters --- crates/kit/src/{sshcred.rs => credentials.rs} | 207 +++++++++--------- crates/kit/src/libvirt/run.rs | 14 +- crates/kit/src/main.rs | 3 +- crates/kit/src/qemu.rs | 2 +- crates/kit/src/run_ephemeral.rs | 2 +- 5 files changed, 114 insertions(+), 114 deletions(-) rename crates/kit/src/{sshcred.rs => credentials.rs} (96%) diff --git a/crates/kit/src/sshcred.rs b/crates/kit/src/credentials.rs similarity index 96% rename from crates/kit/src/sshcred.rs rename to crates/kit/src/credentials.rs index 6c62f1ddf..961540dd3 100644 --- a/crates/kit/src/sshcred.rs +++ b/crates/kit/src/credentials.rs @@ -1,110 +1,11 @@ -//! SSH credential injection for bootc VMs -//! -//! Injects SSH public keys into VMs via systemd credentials using either SMBIOS -//! firmware variables (preferred) or kernel command-line arguments. Creates systemd -//! tmpfiles.d configuration to set up SSH access during VM boot. +//! Systemd credential injection for bootc VMs //! +//! Provides functions for injecting configuration into VMs via systemd credentials +//! using SMBIOS firmware variables (preferred) or kernel command-line arguments. +//! Supports SSH keys, mount units, environment configuration, and AF_VSOCK setup. use color_eyre::Result; -/// Generate SMBIOS credential string for root SSH access -/// -/// Creates a systemd credential for QEMU's SMBIOS interface. Preferred method -/// as it keeps credentials out of kernel command line and boot logs. -/// -/// Returns a string for use with `qemu -smbios type=11,value="..."` -pub fn smbios_cred_for_root_ssh(pubkey: &str) -> Result { - let k = key_to_root_tmpfiles_d(pubkey); - let encoded = data_encoding::BASE64.encode(k.as_bytes()); - let r = format!("io.systemd.credential.binary:tmpfiles.extra={encoded}"); - Ok(r) -} - -/// Generate kernel command-line argument for root SSH access -/// -/// Creates a systemd credential for kernel command-line delivery. Less secure -/// than SMBIOS method as credentials are visible in /proc/cmdline and boot logs. -/// -/// Returns a string for use in kernel boot parameters. -pub fn karg_for_root_ssh(pubkey: &str) -> Result { - let k = key_to_root_tmpfiles_d(pubkey); - let encoded = data_encoding::BASE64.encode(k.as_bytes()); - let r = format!("systemd.set_credential_binary=tmpfiles.extra:{encoded}"); - Ok(r) -} - -/// Convert SSH public key to systemd tmpfiles.d configuration -/// -/// Generates configuration to create `/root/.ssh` directory (0750) and -/// `/root/.ssh/authorized_keys` file (700) with the Base64-encoded SSH key. -/// Uses `f+~` to append to existing authorized_keys files. -pub fn key_to_root_tmpfiles_d(pubkey: &str) -> String { - let buf = data_encoding::BASE64.encode(pubkey.as_bytes()); - format!("d /root/.ssh 0750 - - -\nf+~ /root/.ssh/authorized_keys 700 - - - {buf}\n") -} - -/// Generate SMBIOS credentials for STORAGE_OPTS configuration -/// -/// Creates a systemd unit that conditionally appends STORAGE_OPTS to /etc/environment -/// (for PAM sessions including SSH), plus a dropin to ensure it runs. -/// -/// Returns a vector with: -/// 1. The unit itself (systemd.extra-unit) -/// 2. A dropin for sysinit.target to pull in the unit -pub fn smbios_creds_for_storage_opts() -> Result> { - // Create systemd unit that conditionally appends to /etc/environment - let unit_content = r#"[Unit] -Description=Setup STORAGE_OPTS for bcvk -DefaultDependencies=no -Before=systemd-user-sessions.service - -[Service] -Type=oneshot -ExecStart=/bin/sh -c 'grep -q STORAGE_OPTS /etc/environment || echo STORAGE_OPTS=additionalimagestore=/run/host-container-storage >> /etc/environment' -RemainAfterExit=yes -"#; - let encoded_unit = data_encoding::BASE64.encode(unit_content.as_bytes()); - let unit_cred = format!( - "io.systemd.credential.binary:systemd.extra-unit.bcvk-storage-opts.service={encoded_unit}" - ); - - // Create dropin for sysinit.target to pull in our unit - let dropin_content = "[Unit]\nWants=bcvk-storage-opts.service\n"; - let encoded_dropin = data_encoding::BASE64.encode(dropin_content.as_bytes()); - let dropin_cred = format!( - "io.systemd.credential.binary:systemd.unit-dropin.sysinit.target~bcvk-storage={encoded_dropin}" - ); - - Ok(vec![unit_cred, dropin_cred]) -} - -/// Generate tmpfiles.d lines for STORAGE_OPTS in systemd contexts -/// -/// Configures STORAGE_OPTS for: -/// - /etc/environment.d/: systemd user manager and user services -/// - /etc/systemd/system.conf.d/: system-level systemd services -pub fn storage_opts_tmpfiles_d_lines() -> String { - concat!( - "f /etc/environment.d/90-bcvk-storage.conf 0644 root root - STORAGE_OPTS=additionalimagestore=/run/host-container-storage\n", - "d /etc/systemd/system.conf.d 0755 root root -\n", - "f /etc/systemd/system.conf.d/90-bcvk-storage.conf 0644 root root - [Manager]\\nDefaultEnvironment=STORAGE_OPTS=additionalimagestore=/run/host-container-storage\n" - ).to_string() -} - -/// Generate SMBIOS credential string for AF_VSOCK systemd notification socket -/// -/// Creates a systemd credential that configures systemd to send notifications -/// via AF_VSOCK instead of the default Unix socket. This enables host-guest -/// communication for debugging VM boot sequences. -/// -/// Returns a string for use with `qemu -smbios type=11,value="..."` -pub fn smbios_cred_for_vsock_notify(host_cid: u32, port: u32) -> String { - format!( - "io.systemd.credential:vmm.notify_socket=vsock-stream:{}:{}", - host_cid, port - ) -} - /// Convert a guest mount path to a systemd unit name /// /// Systemd requires mount unit names to match the mount path with: @@ -174,6 +75,7 @@ pub fn generate_mount_unit(virtiofs_tag: &str, guest_path: &str, readonly: bool) /// 2. A dropin for local-fs.target that wants this mount unit /// /// Returns a vector of SMBIOS credential strings +#[allow(dead_code)] pub fn smbios_creds_for_mount_unit( virtiofs_tag: &str, guest_path: &str, @@ -199,6 +101,105 @@ pub fn smbios_creds_for_mount_unit( Ok(vec![mount_cred, dropin_cred]) } +/// Generate SMBIOS credential string for AF_VSOCK systemd notification socket +/// +/// Creates a systemd credential that configures systemd to send notifications +/// via AF_VSOCK instead of the default Unix socket. This enables host-guest +/// communication for debugging VM boot sequences. +/// +/// Returns a string for use with `qemu -smbios type=11,value="..."` +pub fn smbios_cred_for_vsock_notify(host_cid: u32, port: u32) -> String { + format!( + "io.systemd.credential:vmm.notify_socket=vsock-stream:{}:{}", + host_cid, port + ) +} + +/// Generate SMBIOS credentials for STORAGE_OPTS configuration +/// +/// Creates a systemd unit that conditionally appends STORAGE_OPTS to /etc/environment +/// (for PAM sessions including SSH), plus a dropin to ensure it runs. +/// +/// Returns a vector with: +/// 1. The unit itself (systemd.extra-unit) +/// 2. A dropin for sysinit.target to pull in the unit +pub fn smbios_creds_for_storage_opts() -> Result> { + // Create systemd unit that conditionally appends to /etc/environment + let unit_content = r#"[Unit] +Description=Setup STORAGE_OPTS for bcvk +DefaultDependencies=no +Before=systemd-user-sessions.service + +[Service] +Type=oneshot +ExecStart=/bin/sh -c 'grep -q STORAGE_OPTS /etc/environment || echo STORAGE_OPTS=additionalimagestore=/run/host-container-storage >> /etc/environment' +RemainAfterExit=yes +"#; + let encoded_unit = data_encoding::BASE64.encode(unit_content.as_bytes()); + let unit_cred = format!( + "io.systemd.credential.binary:systemd.extra-unit.bcvk-storage-opts.service={encoded_unit}" + ); + + // Create dropin for sysinit.target to pull in our unit + let dropin_content = "[Unit]\nWants=bcvk-storage-opts.service\n"; + let encoded_dropin = data_encoding::BASE64.encode(dropin_content.as_bytes()); + let dropin_cred = format!( + "io.systemd.credential.binary:systemd.unit-dropin.sysinit.target~bcvk-storage={encoded_dropin}" + ); + + Ok(vec![unit_cred, dropin_cred]) +} + +/// Generate tmpfiles.d lines for STORAGE_OPTS in systemd contexts +/// +/// Configures STORAGE_OPTS for: +/// - /etc/environment.d/: systemd user manager and user services +/// - /etc/systemd/system.conf.d/: system-level systemd services +pub fn storage_opts_tmpfiles_d_lines() -> String { + concat!( + "f /etc/environment.d/90-bcvk-storage.conf 0644 root root - STORAGE_OPTS=additionalimagestore=/run/host-container-storage\n", + "d /etc/systemd/system.conf.d 0755 root root -\n", + "f /etc/systemd/system.conf.d/90-bcvk-storage.conf 0644 root root - [Manager]\\nDefaultEnvironment=STORAGE_OPTS=additionalimagestore=/run/host-container-storage\n" + ).to_string() +} + +/// Generate SMBIOS credential string for root SSH access +/// +/// Creates a systemd credential for QEMU's SMBIOS interface. Preferred method +/// as it keeps credentials out of kernel command line and boot logs. +/// +/// Returns a string for use with `qemu -smbios type=11,value="..."` +pub fn smbios_cred_for_root_ssh(pubkey: &str) -> Result { + let k = key_to_root_tmpfiles_d(pubkey); + let encoded = data_encoding::BASE64.encode(k.as_bytes()); + let r = format!("io.systemd.credential.binary:tmpfiles.extra={encoded}"); + Ok(r) +} + +/// Generate kernel command-line argument for root SSH access +/// +/// Creates a systemd credential for kernel command-line delivery. Less secure +/// than SMBIOS method as credentials are visible in /proc/cmdline and boot logs. +/// +/// Returns a string for use in kernel boot parameters. +#[allow(dead_code)] +pub fn karg_for_root_ssh(pubkey: &str) -> Result { + let k = key_to_root_tmpfiles_d(pubkey); + let encoded = data_encoding::BASE64.encode(k.as_bytes()); + let r = format!("systemd.set_credential_binary=tmpfiles.extra:{encoded}"); + Ok(r) +} + +/// Convert SSH public key to systemd tmpfiles.d configuration +/// +/// Generates configuration to create `/root/.ssh` directory (0750) and +/// `/root/.ssh/authorized_keys` file (700) with the Base64-encoded SSH key. +/// Uses `f+~` to append to existing authorized_keys files. +pub fn key_to_root_tmpfiles_d(pubkey: &str) -> String { + let buf = data_encoding::BASE64.encode(pubkey.as_bytes()); + format!("d /root/.ssh 0750 - - -\nf+~ /root/.ssh/authorized_keys 700 - - - {buf}\n") +} + #[cfg(test)] mod tests { use data_encoding::BASE64; diff --git a/crates/kit/src/libvirt/run.rs b/crates/kit/src/libvirt/run.rs index 28edb4ffc..edf973b69 100644 --- a/crates/kit/src/libvirt/run.rs +++ b/crates/kit/src/libvirt/run.rs @@ -736,9 +736,9 @@ fn process_bind_mounts( domain_builder = domain_builder.with_virtiofs_filesystem(virtiofs_fs); // Generate SMBIOS credential for mount unit (without dropin) - let unit_name = crate::sshcred::guest_path_to_unit_name(&bind_mount.guest_path); + let unit_name = crate::credentials::guest_path_to_unit_name(&bind_mount.guest_path); let mount_unit_content = - crate::sshcred::generate_mount_unit(&tag, &bind_mount.guest_path, readonly); + crate::credentials::generate_mount_unit(&tag, &bind_mount.guest_path, readonly); let encoded_mount = data_encoding::BASE64.encode(mount_unit_content.as_bytes()); let mount_cred = format!("io.systemd.credential.binary:systemd.extra-unit.{unit_name}={encoded_mount}"); @@ -916,13 +916,13 @@ fn create_libvirt_domain_from_disk( // Generate SMBIOS credential for SSH key injection and systemd environment configuration // Combine SSH key setup and storage opts for systemd contexts - let mut tmpfiles_content = crate::sshcred::key_to_root_tmpfiles_d(&public_key_content); - tmpfiles_content.push_str(&crate::sshcred::storage_opts_tmpfiles_d_lines()); + let mut tmpfiles_content = crate::credentials::key_to_root_tmpfiles_d(&public_key_content); + tmpfiles_content.push_str(&crate::credentials::storage_opts_tmpfiles_d_lines()); let encoded = data_encoding::BASE64.encode(tmpfiles_content.as_bytes()); let smbios_cred = format!("io.systemd.credential.binary:tmpfiles.extra={encoded}"); // Generate SMBIOS credentials for storage opts unit (handles /etc/environment for PAM/SSH) - let storage_opts_creds = crate::sshcred::smbios_creds_for_storage_opts()?; + let storage_opts_creds = crate::credentials::smbios_creds_for_storage_opts()?; let memory = parse_memory_to_mb(&opts.memory.memory)?; @@ -1064,9 +1064,9 @@ fn create_libvirt_domain_from_disk( // Generate mount unit for automatic mounting at /run/host-container-storage let guest_mount_path = "/run/host-container-storage"; - let unit_name = crate::sshcred::guest_path_to_unit_name(guest_mount_path); + let unit_name = crate::credentials::guest_path_to_unit_name(guest_mount_path); let mount_unit_content = - crate::sshcred::generate_mount_unit("hoststorage", guest_mount_path, true); + crate::credentials::generate_mount_unit("hoststorage", guest_mount_path, true); let encoded_mount = data_encoding::BASE64.encode(mount_unit_content.as_bytes()); let mount_cred = format!("io.systemd.credential.binary:systemd.extra-unit.{unit_name}={encoded_mount}"); diff --git a/crates/kit/src/main.rs b/crates/kit/src/main.rs index 815f092c3..ef5bf3e1d 100644 --- a/crates/kit/src/main.rs +++ b/crates/kit/src/main.rs @@ -13,6 +13,7 @@ mod cli_json; mod common_opts; mod container_entrypoint; pub(crate) mod containerenv; +mod credentials; mod domain_list; mod envdetect; mod ephemeral; @@ -29,8 +30,6 @@ mod qemu_img; mod run_ephemeral; mod run_ephemeral_ssh; mod ssh; -#[allow(dead_code)] -mod sshcred; mod status_monitor; mod supervisor_status; pub(crate) mod systemd; diff --git a/crates/kit/src/qemu.rs b/crates/kit/src/qemu.rs index a057ebe1b..c483c6710 100644 --- a/crates/kit/src/qemu.rs +++ b/crates/kit/src/qemu.rs @@ -845,7 +845,7 @@ impl RunningQemu { let creds = sd_notification .as_ref() .map(|sd| { - let cred = crate::sshcred::smbios_cred_for_vsock_notify(2, sd.port.port()); + let cred = crate::credentials::smbios_cred_for_vsock_notify(2, sd.port.port()); vec![cred] }) .unwrap_or_default(); diff --git a/crates/kit/src/run_ephemeral.rs b/crates/kit/src/run_ephemeral.rs index f1d157800..797a36048 100644 --- a/crates/kit/src/run_ephemeral.rs +++ b/crates/kit/src/run_ephemeral.rs @@ -1003,7 +1003,7 @@ StandardOutput=file:/dev/virtio-ports/executestatus let key_pair = crate::ssh::generate_default_keypair()?; // Create credential and add to kernel args let pubkey = std::fs::read_to_string(key_pair.public_key_path.as_path())?; - let credential = crate::sshcred::smbios_cred_for_root_ssh(&pubkey)?; + let credential = crate::credentials::smbios_cred_for_root_ssh(&pubkey)?; qemu_config.add_smbios_credential(credential); } // Build kernel command line From 3f1e6141b0e01344af4e197dc1a7ac6df045055d Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Thu, 30 Oct 2025 09:40:56 -0400 Subject: [PATCH 2/2] ephemeral: Use SMBIOS credentials for systemd units Fix ephemeral mode to use SMBIOS credentials (systemd.extra-unit.*) instead of writing systemd units directly to `/run/source-image/etc/systemd/system/`. This addresses ConditionFirstBoot issues on Fedora CoreOS where directly written units trigger systemd preset cleanup. The libvirt mode already uses SMBIOS credentials successfully. This change aligns ephemeral mode with that proven approach. Fixes: https://github.com/bootc-dev/bcvk/issues/106 Assisted-by: Claude Code (Sonnet 4.5) Signed-off-by: Colin Walters --- crates/kit/src/run_ephemeral.rs | 188 ++++++++++++++++---------------- crates/kit/src/to_disk.rs | 9 +- 2 files changed, 105 insertions(+), 92 deletions(-) diff --git a/crates/kit/src/run_ephemeral.rs b/crates/kit/src/run_ephemeral.rs index 797a36048..ed1b9fd04 100644 --- a/crates/kit/src/run_ephemeral.rs +++ b/crates/kit/src/run_ephemeral.rs @@ -788,6 +788,8 @@ pub(crate) async fn run_impl(opts: RunEphemeralOpts) -> Result<()> { // Process host mounts and prepare virtiofsd instances for each using async manager let mut additional_mounts = Vec::new(); + // Collect mount unit credentials to inject via SMBIOS instead of writing to filesystem + let mut mount_unit_smbios_creds = Vec::new(); debug!( "Checking for host mounts directory: /run/host-mounts exists = {}", @@ -798,8 +800,7 @@ pub(crate) async fn run_impl(opts: RunEphemeralOpts) -> Result<()> { Utf8Path::new("/run/systemd-units").exists() ); - let target_unitdir = "/run/source-image/etc/systemd/system"; - + let mut mount_unit_names = Vec::new(); if Utf8Path::new("/run/host-mounts").exists() { for entry in fs::read_dir("/run/host-mounts")? { let entry = entry?; @@ -832,73 +833,48 @@ pub(crate) async fn run_impl(opts: RunEphemeralOpts) -> Result<()> { }; additional_mounts.push((virtiofsd_config, tag.clone())); - // Create individual .mount unit for this virtiofs mount + // Generate mount unit via SMBIOS credentials instead of writing to filesystem let mount_point = format!("/run/virtiofs-mnt-{}", mount_name_str); - - // Use systemd-escape to properly escape the mount path - let escaped_path = Command::new("systemd-escape") - .args(["-p", &mount_point]) - .output() - .map(|output| String::from_utf8_lossy(&output.stdout).trim().to_string()) - .unwrap_or_else(|_| { - // Fallback if systemd-escape is not available - mount_point - .replace("/", "-") - .trim_start_matches('-') - .to_string() - }); - - let mount_unit_name = format!("{}.mount", escaped_path); - let mount_options = if is_readonly { "ro" } else { "defaults" }; - - let mount_unit_content = format!( - r#"[Unit] -Description=Mount virtiofs {} -DefaultDependencies=no -After=systemd-remount-fs.service -Before=local-fs.target shutdown.target - -[Mount] -What={} -Where={} -Type=virtiofs -Options={} - -[Install] -WantedBy=local-fs.target -"#, - mount_name_str, tag, mount_point, mount_options + let unit_name = crate::credentials::guest_path_to_unit_name(&mount_point); + let mount_unit_content = + crate::credentials::generate_mount_unit(&tag, &mount_point, is_readonly); + let encoded_mount = data_encoding::BASE64.encode(mount_unit_content.as_bytes()); + + // Create SMBIOS credential for the mount unit + let mount_cred = format!( + "io.systemd.credential.binary:systemd.extra-unit.{unit_name}={encoded_mount}" ); + mount_unit_smbios_creds.push(mount_cred); - let mount_unit_path = format!("{target_unitdir}/{mount_unit_name}"); - fs::write(&mount_unit_path, mount_unit_content) - .with_context(|| format!("Failed to write mount unit to {}", mount_unit_path))?; - - // Enable the mount unit by creating symlink in local-fs.target.wants/ - let wants_dir = format!("{target_unitdir}/local-fs.target.wants"); - fs::create_dir_all(&wants_dir)?; - let wants_link = format!("{}/{}", wants_dir, mount_unit_name); - let relative_target = format!("../{}", mount_unit_name); - std::os::unix::fs::symlink(&relative_target, &wants_link)?; - - // Create mount point directory in the image - let image_mount_point = format!("/run/source-image{}", mount_point); - fs::create_dir_all(&image_mount_point).ok(); + // Collect unit name for the local-fs.target dropin + mount_unit_names.push(unit_name.clone()); debug!( - "Generated mount unit: {} (enabled in local-fs.target)", - mount_unit_name + "Generated SMBIOS credential for mount unit: {} ({})", + unit_name, mode ); } } + // If we have mount units, create a single dropin for local-fs.target + if !mount_unit_names.is_empty() { + let wants_list = mount_unit_names.join(" "); + let dropin_content = format!("[Unit]\nWants={}\n", wants_list); + let encoded_dropin = data_encoding::BASE64.encode(dropin_content.as_bytes()); + let dropin_cred = format!( + "io.systemd.credential.binary:systemd.unit-dropin.local-fs.target~bcvk-mounts={encoded_dropin}" + ); + mount_unit_smbios_creds.push(dropin_cred); + debug!( + "Created local-fs.target dropin for {} mount units", + mount_unit_names.len() + ); + } + // Handle --execute: pipes will be created when adding to qemu_config later // No need to create files anymore as we're using pipes - let default_wantsdir = format!("{target_unitdir}/default.target.wants"); - fs::create_dir_all(&default_wantsdir)?; - - // Create systemd unit to stream journal to virtio-serial device + // Create systemd unit to stream journal to virtio-serial device via SMBIOS credential let journal_stream_unit = r#"[Unit] Description=Stream systemd journal to host via virtio-serial DefaultDependencies=no @@ -916,17 +892,23 @@ RestartSec=1s [Install] WantedBy=sysinit.target "#; - let journal_unit_path = format!("{target_unitdir}/bcvk-journal-stream.service"); - tokio::fs::write(&journal_unit_path, journal_stream_unit).await?; - debug!("Created journal streaming unit at {journal_unit_path}"); - - // Enable the journal streaming unit - let sysinit_wantsdir = format!("{target_unitdir}/sysinit.target.wants"); - tokio::fs::create_dir_all(&sysinit_wantsdir).await?; - let journal_wants_link = format!("{sysinit_wantsdir}/bcvk-journal-stream.service"); - tokio::fs::symlink("../bcvk-journal-stream.service", &journal_wants_link).await?; - debug!("Enabled journal streaming unit in sysinit.target.wants"); + let encoded_journal = data_encoding::BASE64.encode(journal_stream_unit.as_bytes()); + let journal_cred = format!( + "io.systemd.credential.binary:systemd.extra-unit.bcvk-journal-stream.service={encoded_journal}" + ); + mount_unit_smbios_creds.push(journal_cred); + debug!("Generated SMBIOS credential for journal streaming unit"); + + // Create dropin for sysinit.target to enable journal streaming + let journal_dropin = "[Unit]\nWants=bcvk-journal-stream.service\n"; + let encoded_dropin = data_encoding::BASE64.encode(journal_dropin.as_bytes()); + let dropin_cred = format!( + "io.systemd.credential.binary:systemd.unit-dropin.sysinit.target~bcvk-journal={encoded_dropin}" + ); + mount_unit_smbios_creds.push(dropin_cred); + debug!("Created sysinit.target dropin to enable journal streaming unit"); + // Create execute units via SMBIOS credentials if needed match opts.common.execute.as_slice() { [] => {} elts => { @@ -946,8 +928,7 @@ StandardError=inherit service_content.push_str(&format!("ExecStart={elt}\n")); } - let service_finish = format!( - r#"[Unit] + let service_finish = r#"[Unit] Description=Execute Script Service Completion After=bootc-execute.service Requires=dev-virtio\\x2dports-executestatus.device @@ -957,24 +938,34 @@ Type=oneshot ExecStart=systemctl show bootc-execute ExecStart=systemctl poweroff StandardOutput=file:/dev/virtio-ports/executestatus -"# - ); +"#; - let service_path = format!("{target_unitdir}/bootc-execute.service"); - fs::write(service_path, service_content)?; - let service_path = format!("{target_unitdir}/bootc-execute-finish.service"); - fs::write(service_path, service_finish)?; + // Inject execute units via SMBIOS credentials + let encoded_execute = data_encoding::BASE64.encode(service_content.as_bytes()); + let execute_cred = format!( + "io.systemd.credential.binary:systemd.extra-unit.bootc-execute.service={encoded_execute}" + ); + mount_unit_smbios_creds.push(execute_cred); - for svc in ["bootc-execute.service", "bootc-execute-finish.service"] { - let wants_link = format!("{default_wantsdir}/{svc}"); - debug!("Creating execute service symlink: {}", &wants_link); - std::os::unix::fs::symlink(format!("../{svc}"), wants_link)?; - } + let encoded_finish = data_encoding::BASE64.encode(service_finish.as_bytes()); + let finish_cred = format!( + "io.systemd.credential.binary:systemd.extra-unit.bootc-execute-finish.service={encoded_finish}" + ); + mount_unit_smbios_creds.push(finish_cred); + + // Create dropin for default.target to enable execute services + let execute_dropin = + "[Unit]\nWants=bootc-execute.service bootc-execute-finish.service\n"; + let encoded_dropin = data_encoding::BASE64.encode(execute_dropin.as_bytes()); + let dropin_cred = format!( + "io.systemd.credential.binary:systemd.unit-dropin.default.target~bcvk-execute={encoded_dropin}" + ); + mount_unit_smbios_creds.push(dropin_cred); + debug!("Generated SMBIOS credentials for execute units"); } } - // Copy systemd units if provided (after mount units have been generated) - // Also inject if we created mount units that need to be copied + // Copy systemd units if provided (for --systemd-units-dir option) inject_systemd_units()?; // Prepare main virtiofsd config for the source image (will be spawned by QEMU) @@ -1059,22 +1050,30 @@ StandardOutput=file:/dev/virtio-ports/executestatus "swap".into(), crate::to_disk::Format::Raw, ); - let svc = format!( - r#"[Unit] + + // Create swap unit via SMBIOS credential + let svc = r#"[Unit] Description=bcvk ephemeral swap [Swap] What=/dev/disk/by-id/virtio-swap Options= -"# - ); - +"#; let service_name = r#"dev-disk-by\x2did-virtio\x2dswap.swap"#; - let service_path = format!("{target_unitdir}/{service_name}"); - fs::write(&service_path, svc)?; + let encoded_swap = data_encoding::BASE64.encode(svc.as_bytes()); + let swap_cred = format!( + "io.systemd.credential.binary:systemd.extra-unit.{service_name}={encoded_swap}" + ); + mount_unit_smbios_creds.push(swap_cred); - let wants_link = format!("{default_wantsdir}/{service_name}"); - std::os::unix::fs::symlink(format!("../{service_name}"), wants_link)?; + // Create dropin for default.target to enable swap + let swap_dropin = format!("[Unit]\nWants={service_name}\n"); + let encoded_dropin = data_encoding::BASE64.encode(swap_dropin.as_bytes()); + let dropin_cred = format!( + "io.systemd.credential.binary:systemd.unit-dropin.default.target~bcvk-swap={encoded_dropin}" + ); + mount_unit_smbios_creds.push(dropin_cred); + debug!("Generated SMBIOS credential for swap unit"); tmp_swapfile = Some(tmpf); } @@ -1204,6 +1203,13 @@ Options= })?; }; + // Add all SMBIOS credentials for mount units, journal, and execute services + let cred_count = mount_unit_smbios_creds.len(); + for cred in mount_unit_smbios_creds { + qemu_config.add_smbios_credential(cred); + } + debug!("Added {} SMBIOS credentials to QEMU config", cred_count); + debug!("Starting QEMU with systemd debugging enabled"); // Spawn QEMU with all virtiofsd processes handled internally diff --git a/crates/kit/src/to_disk.rs b/crates/kit/src/to_disk.rs index 434aff932..3d85e67af 100644 --- a/crates/kit/src/to_disk.rs +++ b/crates/kit/src/to_disk.rs @@ -226,6 +226,14 @@ impl ToDiskOpts { rm /var/lib/containers -rf ln -sr /var/tmp/containers /var/lib/containers + # Ensure virtiofs mount is available (fallback for older systemd without SMBIOS support) + AIS=/run/virtiofs-mnt-hoststorage/ + if ! mountpoint -q ${AIS} &>/dev/null; then + echo "virtiofs mount not found at ${AIS}, mounting manually..." + mkdir -p ${AIS} + mount -t virtiofs mount_hoststorage ${AIS} -o ro + fi + echo "Starting bootc installation..." echo "Source image: {SOURCE_IMGREF}" echo "Additional args: {BOOTC_ARGS}" @@ -237,7 +245,6 @@ impl ToDiskOpts { # Execute bootc installation, having the outer podman pull from # the virtiofs store on the host, as well as the inner bootc. - AIS=/run/virtiofs-mnt-hoststorage/ export STORAGE_OPTS=additionalimagestore=${AIS} podman run --rm -i ${tty} --privileged --pid=host --net=none -v /sys:/sys:ro \ -v /var/lib/containers:/var/lib/containers -v /dev:/dev -v ${AIS}:${AIS} --security-opt label=type:unconfined_t \