Skip to content
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
207 changes: 104 additions & 103 deletions crates/kit/src/sshcred.rs → crates/kit/src/credentials.rs
Original file line number Diff line number Diff line change
@@ -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<String> {
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<String> {
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<Vec<String>> {
// 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:
Expand Down Expand Up @@ -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,
Expand All @@ -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<Vec<String>> {
// 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<String> {
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<String> {
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;
Expand Down
14 changes: 7 additions & 7 deletions crates/kit/src/libvirt/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
Expand Down Expand Up @@ -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)?;

Expand Down Expand Up @@ -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}");
Expand Down
3 changes: 1 addition & 2 deletions crates/kit/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion crates/kit/src/qemu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading
Loading