diff --git a/Cargo.lock b/Cargo.lock index 95bc528fd..0f88bdb79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -803,6 +803,7 @@ dependencies = [ "fdo-util", "libcryptsetup-rs", "log", + "logtest", "nix", "openssl", "passwd", @@ -1482,6 +1483,7 @@ name = "integration-tests" version = "0.4.10" dependencies = [ "anyhow", + "fdo-client-linuxapp", "fdo-data-formats", "fdo-util", "hex", @@ -1497,6 +1499,8 @@ dependencies = [ "serde", "serde_cbor", "serde_json", + "sha-crypt", + "shadow", "tempfile", "tera", "tokio", @@ -1648,6 +1652,19 @@ name = "log" version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" +dependencies = [ + "value-bag", +] + +[[package]] +name = "logtest" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3e43a8657c1d64516dcc9db8ca03826a4aceaf89d5ce1b37b59f6ff0e43026" +dependencies = [ + "lazy_static", + "log", +] [[package]] name = "maplit" @@ -2641,6 +2658,15 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "shadow" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcaa1dbf13cc052b53af59e70dd467e8e94d6abf8d0eac1f1d15d3e165973b86" +dependencies = [ + "libc", +] + [[package]] name = "shlex" version = "1.1.0" @@ -3194,6 +3220,12 @@ dependencies = [ "getrandom", ] +[[package]] +name = "value-bag" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4d330786735ea358f3bc09eea4caa098569c1c93f342d9aca0514915022fe7e" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/client-linuxapp/Cargo.toml b/client-linuxapp/Cargo.toml index 109aa08a9..fbd4e5861 100644 --- a/client-linuxapp/Cargo.toml +++ b/client-linuxapp/Cargo.toml @@ -22,6 +22,7 @@ secrecy = "0.8" devicemapper = "0.33" openssl = "0.10.45" sha-crypt = "0.5.0" +logtest = "2.0.0" fdo-data-formats = { path = "../data-formats", version = "0.4.10" } fdo-http-wrapper = { path = "../http-wrapper", version = "0.4.10", features = ["client"] } diff --git a/client-linuxapp/src/serviceinfo.rs b/client-linuxapp/src/serviceinfo.rs index ade254d62..da9f4e9d5 100644 --- a/client-linuxapp/src/serviceinfo.rs +++ b/client-linuxapp/src/serviceinfo.rs @@ -21,7 +21,7 @@ use fdo_data_formats::{ }; use fdo_http_wrapper::client::{RequestResult, ServiceClient}; -use sha_crypt::{Sha256Params, sha256_simple, sha256_check}; +use sha_crypt::{sha256_check, sha256_simple, Sha256Params}; const MAX_SERVICE_INFO_LOOPS: u32 = 1000; @@ -65,19 +65,26 @@ fn create_user(user: &str) -> Result<()> { } // Creates new user if user not present log::info!("Creating user: {user}"); - Command::new("useradd") + let status = Command::new("useradd") .arg("-m") .arg(user) .spawn() .context("Error spawning new user command")? .wait() .context("Error creating new user")?; - Ok(()) + + if status.success() { + log::info!("User {user} created successfully"); + Ok(()) + } else { + bail!(format!("User creation failed. Exit Status: {status}")); + } } // Returns true if password is encrypted -// is_password_encrypted functionality is taken from osbuild-composer's crypt.go -fn is_password_encrypted(s: &str) -> bool { +// is_password_encrypted functionality is taken from osbuild-composer's crypt.go: +// https://github.com/osbuild/osbuild-composer/blob/main/internal/crypt/crypt.go +pub fn is_password_encrypted(s: &str) -> bool { let prefixes = ["$2b$", "$6$", "$5$"]; for prefix in prefixes { @@ -105,10 +112,9 @@ fn create_user_with_password(user: &str, password: &str) -> Result<()> { str_encrypted_pw = sha256_simple(password, &default_params).expect("Hashing failed"); assert!(sha256_check(password, &str_encrypted_pw).is_ok()); } - // Creates new user if user not present log::info!("Creating user {user} with password"); - Command::new("useradd") + let status = Command::new("useradd") .arg("-p") .arg(str_encrypted_pw) .arg(user) @@ -116,10 +122,14 @@ fn create_user_with_password(user: &str, password: &str) -> Result<()> { .context("Error spawning new user command")? .wait() .context("Error creating new user")?; - Ok(()) -} - + if status.success() { + log::info!("User {user} created successfully with password"); + Ok(()) + } else { + bail!(format!("User creation failed. Exit Status: {status}")); + } +} fn install_ssh_key(user: &str, key: &str) -> Result<()> { let user_info = passwd::Passwd::from_name(user); if user_info.is_none() { @@ -457,21 +467,15 @@ async fn process_serviceinfo_in(si_in: &ServiceInfo, si_out: &mut ServiceInfo) - } if module == FedoraIotServiceInfoModule::SSHKey.into() { if key == "username" { - let value = value - .as_str() - .context("Error parsing username value")?; + let value = value.as_str().context("Error parsing username value")?; sshkey_user = Some(value.to_string()); log::info!("Username is: {value}"); } else if key == "password" { - let value = value - .as_str() - .context("Error parsing password value")?; + let value = value.as_str().context("Error parsing password value")?; sshkey_password = Some(value.to_string()); log::info!("Password is present"); } else if key == "sshkeys" { - let value = value - .as_str() - .context("Error parsing sshkey value")?; + let value = value.as_str().context("Error parsing sshkey value")?; sshkey_keys = Some(value.to_string()); log::info!("Keys are present"); } @@ -686,14 +690,18 @@ async fn process_serviceinfo_in(si_in: &ServiceInfo, si_out: &mut ServiceInfo) - if sshkey_user.is_none() { bail!("SSHkey module missing username"); } else if sshkey_keys.is_none() && sshkey_password.is_none() { - bail!("SSHkey module missing password and key") + bail!("SSHkey module missing password and key") } if sshkey_password.is_some() { log::info!("SSHkey module was active, creating user with password"); - create_user_with_password(sshkey_user.as_ref().unwrap(), sshkey_password.as_ref().unwrap()).context(format!( + create_user_with_password( + sshkey_user.as_ref().unwrap(), + sshkey_password.as_ref().unwrap(), + ) + .context(format!( "Error creating new user with password: {}", - sshkey_user.as_ref().unwrap() - ))?; + sshkey_user.as_ref().unwrap() + ))?; } if sshkey_keys.is_some() { log::info!("SSHkey module was active, installing SSH keys"); @@ -701,10 +709,15 @@ async fn process_serviceinfo_in(si_in: &ServiceInfo, si_out: &mut ServiceInfo) - "Error creating new user: {}", sshkey_user.as_ref().unwrap() ))?; - let sshkey_keys_v: Vec = sshkey_keys.unwrap().split(" ").map(|s| s.to_string()).collect(); + let sshkey_keys_v: Vec = sshkey_keys + .unwrap() + .split(' ') + .map(|s| s.to_string()) + .collect(); for key in sshkey_keys_v { let key_s: String = key; - install_ssh_key(sshkey_user.as_ref().unwrap(), key_s.as_str()).context("Error installing SSH key")?; + install_ssh_key(sshkey_user.as_ref().unwrap(), key_s.as_str()) + .context("Error installing SSH key")?; log::info!("Installed sshkey: {key_s}"); } } @@ -808,6 +821,8 @@ mod test { use super::BinaryFileInProgress; + use crate::serviceinfo::*; + #[test] fn test_binaryfileinprogress_destination_path() { assert_eq!( @@ -823,4 +838,49 @@ mod test { PathBuf::from("/etc/something") ); } + + #[test] + fn test_pw_encryption() { + let type_5_encryption = "$5$ML4hMHtER3/SY9D2$2eWHscoFbfVebDC32qA2dPo3pD6FFM6CRTrvAOMpwQ"; + assert!(is_password_encrypted(type_5_encryption)); + let type_2b_encryption = "$2b$ML4hMHtER3/SY9D2$2eWHscoFbfVebDC32qA2dPo3pD6FFM6CRTrvAOMpwQ"; + assert!(is_password_encrypted(type_2b_encryption)); + let type_6_encryption = "$6$ML4hMHtER3/SY9D2$2eWHscoFbfVebDC32qA2dPo3pD6FFM6CRTrvAOMpwQ"; + assert!(is_password_encrypted(type_6_encryption)); + let plaintext_encryption = "testpassword"; + assert!(!is_password_encrypted(plaintext_encryption)); + let empty_pw = ""; + assert!(!is_password_encrypted(empty_pw)); + } + + #[test] + fn test_user_creation_no_pw() { + let test_user = "test"; + assert!(create_user(test_user).is_ok()); + let empty_user = ""; + assert!(create_user(empty_user).is_err()); + let at_user = "test@test"; + assert!(create_user(at_user).is_err()); + let dash_user = "-test"; + assert!(create_user(dash_user).is_err()); + let digits_user = "12345"; + assert!(create_user(digits_user).is_err()); + } + + #[test] + fn test_user_creation_with_pw() { + let test_user = "testb"; + let test_password = "password"; + assert!(create_user_with_password(test_user, test_password).is_ok()); + let empty_user = ""; + assert!(create_user_with_password(empty_user, test_password).is_err()); + let at_user = "testb@testb"; + assert!(create_user_with_password(at_user, test_password).is_err()); + let dash_user = "-testb"; + assert!(create_user_with_password(dash_user, test_password).is_err()); + let digits_user = "123456"; + assert!(create_user_with_password(digits_user, test_password).is_err()); + let empty_password = ""; + assert!(create_user_with_password(test_user, empty_password).is_ok()); + } } diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index b3c05158d..7d116f388 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -33,8 +33,11 @@ serde_json = "1.0" pretty_assertions = "1.0.0" paste = "1.0" passwd = "0.0.1" +shadow = "0.0.1" pem = "2.0" users = "0.11.0" +sha-crypt = "0.5.0" fdo-data-formats = { path = "../data-formats" } fdo-util = { path = "../util" } +fdo-client-linuxapp = { path = "../client-linuxapp"} \ No newline at end of file diff --git a/integration-tests/templates/serviceinfo-api-server.yml.j2 b/integration-tests/templates/serviceinfo-api-server.yml.j2 index e12fe146a..581b592b9 100644 --- a/integration-tests/templates/serviceinfo-api-server.yml.j2 +++ b/integration-tests/templates/serviceinfo-api-server.yml.j2 @@ -8,7 +8,7 @@ admin_auth_token: TestAdminToken service_info: initial_user: username: {{ user }} - password: test + password: {{ password }} sshkeys: - {{ sshkey }} files: diff --git a/integration-tests/tests/common/mod.rs b/integration-tests/tests/common/mod.rs index 2b683dee5..d16dc4272 100644 --- a/integration-tests/tests/common/mod.rs +++ b/integration-tests/tests/common/mod.rs @@ -808,6 +808,7 @@ impl<'a> TestServerConfigurator<'a> { users::get_current_username().unwrap().to_str().unwrap(), ); cfg.insert("sshkey", "sshkey_default"); + cfg.insert("password", "testpassword"); } else { L.l("per_device_serviceinfo is set, using device specific values"); cfg.insert( @@ -815,6 +816,7 @@ impl<'a> TestServerConfigurator<'a> { users::get_current_username().unwrap().to_str().unwrap(), ); cfg.insert("sshkey", "sshkey_per_device"); + cfg.insert("password", "testpassword"); } // TODO: Insert more defaults diff --git a/integration-tests/tests/e2e.rs b/integration-tests/tests/e2e.rs index 63792e8b9..4d12a3c4e 100644 --- a/integration-tests/tests/e2e.rs +++ b/integration-tests/tests/e2e.rs @@ -7,6 +7,8 @@ use common::{Binary, LogSide, TestContext}; use anyhow::{bail, Context, Result}; +use sha_crypt::sha256_check; + const L: LogSide = LogSide::Test; #[tokio::test] @@ -138,6 +140,7 @@ where env::set_var("PER_DEVICE_SERVICEINFO", "false"); let mut ctx = TestContext::new().context("Error building test context")?; let new_user: &str = "testuser"; // new user to be created during onboarding + let new_pw: &str = "testpassword"; // new password to accompany new user during onboarding let encrypted_disk_loc = ctx.testpath().join("encrypted.img"); let rendezvous_server = ctx .start_test_server( @@ -156,7 +159,8 @@ where &encrypted_disk_loc.to_string_lossy(), ); if ci { - cfg.insert("user", new_user) + cfg.insert("user", new_user); + cfg.insert("password", new_pw); }; Ok(()) })?) @@ -402,6 +406,14 @@ sshkey_default "User: {} is not created during onboarding", &new_user ); + if let Some(test_user) = shadow::Shadow::from_name(new_user) { + pretty_assertions::assert_eq!( + test_user.password.is_empty(), + false, + "Password not created during onboarding" + ); + assert!(sha256_check("testpassword", &test_user.password).is_ok()); + } } else { L.l("Skipped create initial user validation To validate set env variable FDO_PRIVILEGED and run test as superuser"); diff --git a/integration-tests/tests/service_info.rs b/integration-tests/tests/service_info.rs new file mode 100644 index 000000000..1eea6c3a9 --- /dev/null +++ b/integration-tests/tests/service_info.rs @@ -0,0 +1,331 @@ +mod common; +use std::env; +#[allow(unused_imports)] +use std::{fs, io::Write, process::Command, time::Duration}; + +use common::{Binary, LogSide, TestContext}; + +use anyhow::{bail, Context, Result}; + +use sha_crypt::sha256_check; + +const L: LogSide = LogSide::Test; + +#[tokio::test] +async fn testpw_testpassword() -> Result<()> { + env::set_var("PER_DEVICE_SERVICEINFO", "false"); + + test_e2e_impl_default_serviceinfo( + |_| { + Ok(( + "DIUN_PUB_KEY_INSECURE", + "true".to_string(), + "Trusting any certificate as root", + )) + }, + "FileSystem", + "testuser", + "testpassword", + ) + .await +} + +#[derive(Debug)] +struct TestCase { + #[allow(dead_code)] + diun_verification_method_name: &'static str, + #[allow(dead_code)] + diun_key_type: &'static str, + #[allow(dead_code)] + error: anyhow::Error, +} + +async fn test_e2e_impl_default_serviceinfo( + verification_generator: F, + diun_key_type: &str, + test_user_string: &str, + test_pw_string: &str, +) -> Result<()> +where + F: Fn(&TestContext) -> Result<(&'static str, String, &'static str)>, +{ + let ci = env::var("FDO_PRIVILEGED").is_ok(); + env::set_var("PER_DEVICE_SERVICEINFO", "false"); + let mut ctx = TestContext::new().context("Error building test context")?; + let new_user: &str = test_user_string; // new user to be created during onboarding + let new_pw: &str = test_pw_string; // new password to accompany new user during onboarding + let encrypted_disk_loc = ctx.testpath().join("encrypted.img"); + let rendezvous_server = ctx + .start_test_server( + Binary::RendezvousServer, + |cfg| Ok(cfg.prepare_config_file(None, |_| Ok(()))?), + |_| Ok(()), + ) + .context("Error creating rendezvous server")?; + let serviceinfo_api_server = ctx + .start_test_server( + Binary::ServiceInfoApiServer, + |cfg| { + Ok(cfg.prepare_config_file(None, |cfg| { + cfg.insert( + "encrypted_disk_label", + &encrypted_disk_loc.to_string_lossy(), + ); + if ci { + cfg.insert("user", new_user); + cfg.insert("password", new_pw); + }; + Ok(()) + })?) + }, + |_| Ok(()), + ) + .context("Error creating serviceinfo API dev server")?; + let owner_onboarding_server = ctx + .start_test_server( + Binary::OwnerOnboardingServer, + |cfg| { + Ok(cfg.prepare_config_file(None, |cfg| { + cfg.insert( + "serviceinfo_api_server_port", + &serviceinfo_api_server.server_port().unwrap(), + ); + Ok(()) + })?) + }, + |cmd| { + cmd.env("ALLOW_NONINTEROPERABLE_KDF", &"1"); + Ok(()) + }, + ) + .context("Error creating owner server")?; + let mfg_server = ctx + .start_test_server( + Binary::ManufacturingServer, + |cfg| { + Ok(cfg.prepare_config_file(None, |cfg| { + cfg.insert("diun_key_type", diun_key_type); + cfg.insert("rendezvous_port", &rendezvous_server.server_port().unwrap()); + cfg.insert("device_identification_format", "SerialNumber"); + Ok(()) + })?) + }, + |_| Ok(()), + ) + .context("Error creating manufacturing server")?; + ctx.wait_until_servers_ready() + .await + .context("Error waiting for servers to start")?; + + let (verification_key, verification_value, verification_searchstr) = + verification_generator(&ctx).context("Error generating verification information")?; + + // Execute the DI(UN) protocols + let client_result = ctx + .run_client( + Binary::ManufacturingClient, + Some(&mfg_server), + |cfg| { + cfg.env("DEVICE_CREDENTIAL_FILENAME", "devicecredential.dc") + .env("MANUFACTURING_INFO", "testdevice") + .env(&verification_key, &verification_value); + Ok(()) + }, + Duration::from_secs(5), + ) + .context("Error running manufacturing client")?; + client_result + .expect_success() + .context("Manufacturing client failed")?; + client_result.expect_stderr_line(verification_searchstr)?; + + // Execute some tests on the device credential and ownership voucher + let dc_path = client_result.client_path().join("devicecredential.dc"); + L.l(format!("Device Credential should be in {:?}", dc_path)); + + let ov_dir = ctx.testpath().join("ownership_vouchers"); + let mut ov_files = + std::fs::read_dir(ov_dir).context("Error reading ownership voucher directory")?; + L.l(format!("Ownership Voucher files: {:?}", &ov_files)); + let ov_file = ov_files.next(); + if ov_file.is_none() { + bail!("No ownership voucher files found"); + } + let ov_file = ov_file + .unwrap() + .context("Error reading OV file directory")?; + let num_count = ov_files.count() + 1; // The +1 is because we consumed the first item + if num_count != 1 { + bail!( + "Invalid number of ownership vouchers: {}, expected 1", + num_count + ); + } + L.l(format!("Ownership voucher path: {:?}", ov_file)); + let device_guid = ov_file.file_name().to_str().unwrap().to_string(); + L.l(format!("Device GUID: {}", device_guid)); + + L.l("Adding disk encryption tests"); + L.l("Creating empty disk image"); + if !Command::new("truncate") + .arg("-s") + .arg("1G") + .arg(&encrypted_disk_loc) + .status() + .context("Error running truncate")? + .success() + { + bail!("Error creating empty disk image"); + } + + L.l("Encrypting disk image"); + let mut child = Command::new("cryptsetup") + .arg("luksFormat") + .arg(&encrypted_disk_loc) + .arg("--force-password") + .stdin(std::process::Stdio::piped()) + .spawn() + .context("Error starting cryptsetup luksFormat")?; + { + let mut stdin = child.stdin.take().context("Error taking stdin")?; + writeln!(stdin, "testpassword")?; + stdin.flush()?; + } + + let output = child.wait().context("Error waiting for cryptsetup")?; + if !output.success() { + bail!("Failed to call cryptsetup"); + } + + L.l("Binding disk image"); + let mut child = Command::new("clevis") + .arg("luks") + .arg("bind") + .arg("-d") + .arg(&encrypted_disk_loc) + .arg("test") + .arg("{}") + .env("PATH", ctx.get_path_env()?) + .stdin(std::process::Stdio::piped()) + .spawn() + .context("Error starting clevis luks bind")?; + { + let mut stdin = child.stdin.take().context("Error taking stdin")?; + writeln!(stdin, "testpassword")?; + stdin.flush()?; + } + + let output = child.wait().context("Error waiting for clevis to bind")?; + if !output.success() { + bail!("Failed to call clevis luks bind"); + } + + let client = reqwest::Client::new(); + + // Ensure TO0 is executed + let res = client + .post(format!( + "http://localhost:{}/report-to-rendezvous", // DevSkim: ignore DS137138 + owner_onboarding_server.server_port().unwrap() + )) + .send() + .await?; + L.l(format!("Status code report-to-rendezvous {}", res.status())); + + // Execute TO1/TO2 protocols + let ssh_authorized_keys_path = ctx.testpath().join("authorized_keys"); + let marker_file_path = ctx.testpath().join("marker"); + let binary_file_path_prefix = ctx.testpath().join("binary_files"); + + std::fs::create_dir(&binary_file_path_prefix).context("Error creating binary_files dir")?; + + let output = ctx + .run_client( + Binary::ClientLinuxapp, + None, + |cfg| { + cfg.env("DEVICE_CREDENTIAL", dc_path.to_str().unwrap()) + .env("SSH_KEY_PATH", &ssh_authorized_keys_path.to_str().unwrap()) + .env( + "BINARYFILE_PATH_PREFIX", + binary_file_path_prefix.to_str().unwrap(), + ) + .env( + "DEVICE_ONBOARDING_EXECUTED_MARKER_FILE_PATH", + &marker_file_path.to_str().unwrap(), + ) + .env("ALLOW_NONINTEROPERABLE_KDF", &"1"); + Ok(()) + }, + Duration::from_secs(60), + ) + .context("Error running client")?; + output.expect_success().context("client failed")?; + + pretty_assertions::assert_eq!( + fs::read_to_string(&marker_file_path).context("Error reading marker file")?, + "executed" + ); + pretty_assertions::assert_eq!( + fs::read_to_string(&ssh_authorized_keys_path) + .context("Error reading authorized SSH keys")?, + " +# These keys are installed by FIDO Device Onboarding +sshkey_default +# End of FIDO Device Onboarding keys +" + ); + if ci { + L.l("Running create initial user validation"); + pretty_assertions::assert_eq!( + passwd::Passwd::from_name(new_user).is_some(), + true, + "User: {} is not created during onboarding", + &new_user + ); + if let Some(test_user) = shadow::Shadow::from_name(new_user) { + pretty_assertions::assert_eq!( + test_user.password.is_empty(), + false, + "Password not created during onboarding" + ); + assert!(sha256_check("testpassword", &test_user.password).is_ok()); + } + } else { + L.l("Skipped create initial user validation + To validate set env variable FDO_PRIVILEGED and run test as superuser"); + } + + L.l("Checking encrypted disk image"); + let output = Command::new("cryptsetup") + .arg("luksDump") + .arg(encrypted_disk_loc) + .output() + .context("Error running cryptsetup")?; + if !output.status.success() { + bail!("Failed to call cryptsetup"); + } + let luksdump_stdout = + String::from_utf8(output.stdout).context("Error reading luksDump stdout")?; + L.l(format!("Cryptsetup luksDump output: {:?}", luksdump_stdout)); + let mut found_ds_backup_final = false; + let mut found_reencrypt_unbound = false; + for stdout_line in luksdump_stdout.split('\n') { + if stdout_line.contains("flags") && stdout_line.contains("backup-final") { + found_ds_backup_final = true; + continue; + } + if stdout_line.contains("reencrypt (unbound)") { + found_reencrypt_unbound = true; + continue; + } + } + if !found_ds_backup_final { + bail!("Failed to find backup-final flag in cryptsetup output"); + } + if !found_reencrypt_unbound { + bail!("Failed to find reencrypt (unbound) flag in cryptsetup output"); + } + + Ok(()) +} diff --git a/owner-onboarding-server/src/handlers.rs b/owner-onboarding-server/src/handlers.rs index d065c8c1a..b8a389134 100644 --- a/owner-onboarding-server/src/handlers.rs +++ b/owner-onboarding-server/src/handlers.rs @@ -540,8 +540,8 @@ async fn perform_service_info( )?; if initial_user.password.is_some() { out_si.add( - FedoraIotServiceInfoModule::SSHKey, - "password", + FedoraIotServiceInfoModule::SSHKey, + "password", &initial_user.password, )?; } diff --git a/serviceinfo-api-server/src/main.rs b/serviceinfo-api-server/src/main.rs index 38bb9e939..ec0dc6f64 100644 --- a/serviceinfo-api-server/src/main.rs +++ b/serviceinfo-api-server/src/main.rs @@ -234,6 +234,7 @@ async fn serviceinfo_handler( if let Some(initial_user) = &per_device_settings.initial_user { reply.reply.initial_user = Some(ServiceInfoApiReplyInitialUser { username: initial_user.username.clone(), + password: initial_user.password.clone(), ssh_keys: initial_user.sshkeys.clone(), }); } @@ -246,6 +247,7 @@ async fn serviceinfo_handler( log::debug!("serviceinfo setting from base file applied"); reply.reply.initial_user = Some(ServiceInfoApiReplyInitialUser { username: initial_user.username.clone(), + password: initial_user.password.clone(), ssh_keys: initial_user.sshkeys.clone(), }); }