Skip to content

Commit c954cb3

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

File tree

5 files changed

+114
-114
lines changed

5 files changed

+114
-114
lines changed
Lines changed: 104 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,110 +1,11 @@
1-
//! SSH credential injection for bootc VMs
2-
//!
3-
//! Injects SSH public keys into VMs via systemd credentials using either SMBIOS
4-
//! firmware variables (preferred) or kernel command-line arguments. Creates systemd
5-
//! tmpfiles.d configuration to set up SSH access during VM boot.
1+
//! Systemd credential injection for bootc VMs
62
//!
3+
//! Provides functions for injecting configuration into VMs via systemd credentials
4+
//! using SMBIOS firmware variables (preferred) or kernel command-line arguments.
5+
//! Supports SSH keys, mount units, environment configuration, and AF_VSOCK setup.
76
87
use color_eyre::Result;
98

10-
/// Generate SMBIOS credential string for root SSH access
11-
///
12-
/// Creates a systemd credential for QEMU's SMBIOS interface. Preferred method
13-
/// as it keeps credentials out of kernel command line and boot logs.
14-
///
15-
/// Returns a string for use with `qemu -smbios type=11,value="..."`
16-
pub fn smbios_cred_for_root_ssh(pubkey: &str) -> Result<String> {
17-
let k = key_to_root_tmpfiles_d(pubkey);
18-
let encoded = data_encoding::BASE64.encode(k.as_bytes());
19-
let r = format!("io.systemd.credential.binary:tmpfiles.extra={encoded}");
20-
Ok(r)
21-
}
22-
23-
/// Generate kernel command-line argument for root SSH access
24-
///
25-
/// Creates a systemd credential for kernel command-line delivery. Less secure
26-
/// than SMBIOS method as credentials are visible in /proc/cmdline and boot logs.
27-
///
28-
/// Returns a string for use in kernel boot parameters.
29-
pub fn karg_for_root_ssh(pubkey: &str) -> Result<String> {
30-
let k = key_to_root_tmpfiles_d(pubkey);
31-
let encoded = data_encoding::BASE64.encode(k.as_bytes());
32-
let r = format!("systemd.set_credential_binary=tmpfiles.extra:{encoded}");
33-
Ok(r)
34-
}
35-
36-
/// Convert SSH public key to systemd tmpfiles.d configuration
37-
///
38-
/// Generates configuration to create `/root/.ssh` directory (0750) and
39-
/// `/root/.ssh/authorized_keys` file (700) with the Base64-encoded SSH key.
40-
/// Uses `f+~` to append to existing authorized_keys files.
41-
pub fn key_to_root_tmpfiles_d(pubkey: &str) -> String {
42-
let buf = data_encoding::BASE64.encode(pubkey.as_bytes());
43-
format!("d /root/.ssh 0750 - - -\nf+~ /root/.ssh/authorized_keys 700 - - - {buf}\n")
44-
}
45-
46-
/// Generate SMBIOS credentials for STORAGE_OPTS configuration
47-
///
48-
/// Creates a systemd unit that conditionally appends STORAGE_OPTS to /etc/environment
49-
/// (for PAM sessions including SSH), plus a dropin to ensure it runs.
50-
///
51-
/// Returns a vector with:
52-
/// 1. The unit itself (systemd.extra-unit)
53-
/// 2. A dropin for sysinit.target to pull in the unit
54-
pub fn smbios_creds_for_storage_opts() -> Result<Vec<String>> {
55-
// Create systemd unit that conditionally appends to /etc/environment
56-
let unit_content = r#"[Unit]
57-
Description=Setup STORAGE_OPTS for bcvk
58-
DefaultDependencies=no
59-
Before=systemd-user-sessions.service
60-
61-
[Service]
62-
Type=oneshot
63-
ExecStart=/bin/sh -c 'grep -q STORAGE_OPTS /etc/environment || echo STORAGE_OPTS=additionalimagestore=/run/host-container-storage >> /etc/environment'
64-
RemainAfterExit=yes
65-
"#;
66-
let encoded_unit = data_encoding::BASE64.encode(unit_content.as_bytes());
67-
let unit_cred = format!(
68-
"io.systemd.credential.binary:systemd.extra-unit.bcvk-storage-opts.service={encoded_unit}"
69-
);
70-
71-
// Create dropin for sysinit.target to pull in our unit
72-
let dropin_content = "[Unit]\nWants=bcvk-storage-opts.service\n";
73-
let encoded_dropin = data_encoding::BASE64.encode(dropin_content.as_bytes());
74-
let dropin_cred = format!(
75-
"io.systemd.credential.binary:systemd.unit-dropin.sysinit.target~bcvk-storage={encoded_dropin}"
76-
);
77-
78-
Ok(vec![unit_cred, dropin_cred])
79-
}
80-
81-
/// Generate tmpfiles.d lines for STORAGE_OPTS in systemd contexts
82-
///
83-
/// Configures STORAGE_OPTS for:
84-
/// - /etc/environment.d/: systemd user manager and user services
85-
/// - /etc/systemd/system.conf.d/: system-level systemd services
86-
pub fn storage_opts_tmpfiles_d_lines() -> String {
87-
concat!(
88-
"f /etc/environment.d/90-bcvk-storage.conf 0644 root root - STORAGE_OPTS=additionalimagestore=/run/host-container-storage\n",
89-
"d /etc/systemd/system.conf.d 0755 root root -\n",
90-
"f /etc/systemd/system.conf.d/90-bcvk-storage.conf 0644 root root - [Manager]\\nDefaultEnvironment=STORAGE_OPTS=additionalimagestore=/run/host-container-storage\n"
91-
).to_string()
92-
}
93-
94-
/// Generate SMBIOS credential string for AF_VSOCK systemd notification socket
95-
///
96-
/// Creates a systemd credential that configures systemd to send notifications
97-
/// via AF_VSOCK instead of the default Unix socket. This enables host-guest
98-
/// communication for debugging VM boot sequences.
99-
///
100-
/// Returns a string for use with `qemu -smbios type=11,value="..."`
101-
pub fn smbios_cred_for_vsock_notify(host_cid: u32, port: u32) -> String {
102-
format!(
103-
"io.systemd.credential:vmm.notify_socket=vsock-stream:{}:{}",
104-
host_cid, port
105-
)
106-
}
107-
1089
/// Convert a guest mount path to a systemd unit name
10910
///
11011
/// 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)
17475
/// 2. A dropin for local-fs.target that wants this mount unit
17576
///
17677
/// Returns a vector of SMBIOS credential strings
78+
#[allow(dead_code)]
17779
pub fn smbios_creds_for_mount_unit(
17880
virtiofs_tag: &str,
17981
guest_path: &str,
@@ -199,6 +101,105 @@ pub fn smbios_creds_for_mount_unit(
199101
Ok(vec![mount_cred, dropin_cred])
200102
}
201103

104+
/// Generate SMBIOS credential string for AF_VSOCK systemd notification socket
105+
///
106+
/// Creates a systemd credential that configures systemd to send notifications
107+
/// via AF_VSOCK instead of the default Unix socket. This enables host-guest
108+
/// communication for debugging VM boot sequences.
109+
///
110+
/// Returns a string for use with `qemu -smbios type=11,value="..."`
111+
pub fn smbios_cred_for_vsock_notify(host_cid: u32, port: u32) -> String {
112+
format!(
113+
"io.systemd.credential:vmm.notify_socket=vsock-stream:{}:{}",
114+
host_cid, port
115+
)
116+
}
117+
118+
/// Generate SMBIOS credentials for STORAGE_OPTS configuration
119+
///
120+
/// Creates a systemd unit that conditionally appends STORAGE_OPTS to /etc/environment
121+
/// (for PAM sessions including SSH), plus a dropin to ensure it runs.
122+
///
123+
/// Returns a vector with:
124+
/// 1. The unit itself (systemd.extra-unit)
125+
/// 2. A dropin for sysinit.target to pull in the unit
126+
pub fn smbios_creds_for_storage_opts() -> Result<Vec<String>> {
127+
// Create systemd unit that conditionally appends to /etc/environment
128+
let unit_content = r#"[Unit]
129+
Description=Setup STORAGE_OPTS for bcvk
130+
DefaultDependencies=no
131+
Before=systemd-user-sessions.service
132+
133+
[Service]
134+
Type=oneshot
135+
ExecStart=/bin/sh -c 'grep -q STORAGE_OPTS /etc/environment || echo STORAGE_OPTS=additionalimagestore=/run/host-container-storage >> /etc/environment'
136+
RemainAfterExit=yes
137+
"#;
138+
let encoded_unit = data_encoding::BASE64.encode(unit_content.as_bytes());
139+
let unit_cred = format!(
140+
"io.systemd.credential.binary:systemd.extra-unit.bcvk-storage-opts.service={encoded_unit}"
141+
);
142+
143+
// Create dropin for sysinit.target to pull in our unit
144+
let dropin_content = "[Unit]\nWants=bcvk-storage-opts.service\n";
145+
let encoded_dropin = data_encoding::BASE64.encode(dropin_content.as_bytes());
146+
let dropin_cred = format!(
147+
"io.systemd.credential.binary:systemd.unit-dropin.sysinit.target~bcvk-storage={encoded_dropin}"
148+
);
149+
150+
Ok(vec![unit_cred, dropin_cred])
151+
}
152+
153+
/// Generate tmpfiles.d lines for STORAGE_OPTS in systemd contexts
154+
///
155+
/// Configures STORAGE_OPTS for:
156+
/// - /etc/environment.d/: systemd user manager and user services
157+
/// - /etc/systemd/system.conf.d/: system-level systemd services
158+
pub fn storage_opts_tmpfiles_d_lines() -> String {
159+
concat!(
160+
"f /etc/environment.d/90-bcvk-storage.conf 0644 root root - STORAGE_OPTS=additionalimagestore=/run/host-container-storage\n",
161+
"d /etc/systemd/system.conf.d 0755 root root -\n",
162+
"f /etc/systemd/system.conf.d/90-bcvk-storage.conf 0644 root root - [Manager]\\nDefaultEnvironment=STORAGE_OPTS=additionalimagestore=/run/host-container-storage\n"
163+
).to_string()
164+
}
165+
166+
/// Generate SMBIOS credential string for root SSH access
167+
///
168+
/// Creates a systemd credential for QEMU's SMBIOS interface. Preferred method
169+
/// as it keeps credentials out of kernel command line and boot logs.
170+
///
171+
/// Returns a string for use with `qemu -smbios type=11,value="..."`
172+
pub fn smbios_cred_for_root_ssh(pubkey: &str) -> Result<String> {
173+
let k = key_to_root_tmpfiles_d(pubkey);
174+
let encoded = data_encoding::BASE64.encode(k.as_bytes());
175+
let r = format!("io.systemd.credential.binary:tmpfiles.extra={encoded}");
176+
Ok(r)
177+
}
178+
179+
/// Generate kernel command-line argument for root SSH access
180+
///
181+
/// Creates a systemd credential for kernel command-line delivery. Less secure
182+
/// than SMBIOS method as credentials are visible in /proc/cmdline and boot logs.
183+
///
184+
/// Returns a string for use in kernel boot parameters.
185+
#[allow(dead_code)]
186+
pub fn karg_for_root_ssh(pubkey: &str) -> Result<String> {
187+
let k = key_to_root_tmpfiles_d(pubkey);
188+
let encoded = data_encoding::BASE64.encode(k.as_bytes());
189+
let r = format!("systemd.set_credential_binary=tmpfiles.extra:{encoded}");
190+
Ok(r)
191+
}
192+
193+
/// Convert SSH public key to systemd tmpfiles.d configuration
194+
///
195+
/// Generates configuration to create `/root/.ssh` directory (0750) and
196+
/// `/root/.ssh/authorized_keys` file (700) with the Base64-encoded SSH key.
197+
/// Uses `f+~` to append to existing authorized_keys files.
198+
pub fn key_to_root_tmpfiles_d(pubkey: &str) -> String {
199+
let buf = data_encoding::BASE64.encode(pubkey.as_bytes());
200+
format!("d /root/.ssh 0750 - - -\nf+~ /root/.ssh/authorized_keys 700 - - - {buf}\n")
201+
}
202+
202203
#[cfg(test)]
203204
mod tests {
204205
use data_encoding::BASE64;

crates/kit/src/libvirt/run.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -750,9 +750,9 @@ fn process_bind_mounts(
750750
domain_builder = domain_builder.with_virtiofs_filesystem(virtiofs_fs);
751751

752752
// Generate SMBIOS credential for mount unit (without dropin)
753-
let unit_name = crate::sshcred::guest_path_to_unit_name(&bind_mount.guest_path);
753+
let unit_name = crate::credentials::guest_path_to_unit_name(&bind_mount.guest_path);
754754
let mount_unit_content =
755-
crate::sshcred::generate_mount_unit(&tag, &bind_mount.guest_path, readonly);
755+
crate::credentials::generate_mount_unit(&tag, &bind_mount.guest_path, readonly);
756756
let encoded_mount = data_encoding::BASE64.encode(mount_unit_content.as_bytes());
757757
let mount_cred =
758758
format!("io.systemd.credential.binary:systemd.extra-unit.{unit_name}={encoded_mount}");
@@ -930,13 +930,13 @@ fn create_libvirt_domain_from_disk(
930930

931931
// Generate SMBIOS credential for SSH key injection and systemd environment configuration
932932
// Combine SSH key setup and storage opts for systemd contexts
933-
let mut tmpfiles_content = crate::sshcred::key_to_root_tmpfiles_d(&public_key_content);
934-
tmpfiles_content.push_str(&crate::sshcred::storage_opts_tmpfiles_d_lines());
933+
let mut tmpfiles_content = crate::credentials::key_to_root_tmpfiles_d(&public_key_content);
934+
tmpfiles_content.push_str(&crate::credentials::storage_opts_tmpfiles_d_lines());
935935
let encoded = data_encoding::BASE64.encode(tmpfiles_content.as_bytes());
936936
let smbios_cred = format!("io.systemd.credential.binary:tmpfiles.extra={encoded}");
937937

938938
// Generate SMBIOS credentials for storage opts unit (handles /etc/environment for PAM/SSH)
939-
let storage_opts_creds = crate::sshcred::smbios_creds_for_storage_opts()?;
939+
let storage_opts_creds = crate::credentials::smbios_creds_for_storage_opts()?;
940940

941941
let memory = parse_memory_to_mb(&opts.memory.memory)?;
942942

@@ -1078,9 +1078,9 @@ fn create_libvirt_domain_from_disk(
10781078

10791079
// Generate mount unit for automatic mounting at /run/host-container-storage
10801080
let guest_mount_path = "/run/host-container-storage";
1081-
let unit_name = crate::sshcred::guest_path_to_unit_name(guest_mount_path);
1081+
let unit_name = crate::credentials::guest_path_to_unit_name(guest_mount_path);
10821082
let mount_unit_content =
1083-
crate::sshcred::generate_mount_unit("hoststorage", guest_mount_path, true);
1083+
crate::credentials::generate_mount_unit("hoststorage", guest_mount_path, true);
10841084
let encoded_mount = data_encoding::BASE64.encode(mount_unit_content.as_bytes());
10851085
let mount_cred =
10861086
format!("io.systemd.credential.binary:systemd.extra-unit.{unit_name}={encoded_mount}");

crates/kit/src/main.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ mod cli_json;
1313
mod common_opts;
1414
mod container_entrypoint;
1515
pub(crate) mod containerenv;
16+
mod credentials;
1617
mod domain_list;
1718
mod envdetect;
1819
mod ephemeral;
@@ -29,8 +30,6 @@ mod qemu_img;
2930
mod run_ephemeral;
3031
mod run_ephemeral_ssh;
3132
mod ssh;
32-
#[allow(dead_code)]
33-
mod sshcred;
3433
mod status_monitor;
3534
mod supervisor_status;
3635
pub(crate) mod systemd;

crates/kit/src/qemu.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -845,7 +845,7 @@ impl RunningQemu {
845845
let creds = sd_notification
846846
.as_ref()
847847
.map(|sd| {
848-
let cred = crate::sshcred::smbios_cred_for_vsock_notify(2, sd.port.port());
848+
let cred = crate::credentials::smbios_cred_for_vsock_notify(2, sd.port.port());
849849
vec![cred]
850850
})
851851
.unwrap_or_default();

crates/kit/src/run_ephemeral.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1006,7 +1006,7 @@ StandardOutput=file:/dev/virtio-ports/executestatus
10061006
let key_pair = crate::ssh::generate_default_keypair()?;
10071007
// Create credential and add to kernel args
10081008
let pubkey = std::fs::read_to_string(key_pair.public_key_path.as_path())?;
1009-
let credential = crate::sshcred::smbios_cred_for_root_ssh(&pubkey)?;
1009+
let credential = crate::credentials::smbios_cred_for_root_ssh(&pubkey)?;
10101010
qemu_config.add_smbios_credential(credential);
10111011
}
10121012
// Build kernel command line

0 commit comments

Comments
 (0)