Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
b4585cf
Cleanup: "unused" warnings
mchf Mar 12, 2026
c9c5a4c
Refactoring: function for storing ssh keys modified to handle vector
mchf Mar 12, 2026
9412e85
Store ssh keys for user if any
mchf Mar 12, 2026
5295715
Handling of multiple ssh keys for root in users service
mchf Mar 12, 2026
1c4851f
Updated schema
mchf Mar 12, 2026
5012534
Modified AutoYast root reader to convert list of ssh keys
mchf Mar 13, 2026
c843ba0
Modified AutoYast user reader to allow conversion of authorized keys
mchf Mar 13, 2026
8213b9a
Happy rubocop
mchf Mar 13, 2026
3b3c78f
Fixed FirsUserConfig test
mchf Mar 13, 2026
33b371e
Fixed ruby tests
mchf Mar 13, 2026
eb4cbac
Fixed FirstUserConfig test
mchf Mar 13, 2026
b14d897
Merge branch 'master' into multiple_ssh_keys
mchf Mar 13, 2026
9012bef
Made sshPublicKeys an alias for sshPublicKey in case of root user
mchf Mar 13, 2026
13d46a7
Fixed tests
mchf Mar 13, 2026
d13f719
Formatting
mchf Mar 13, 2026
4a6f56d
Fixed OpenApi
mchf Mar 13, 2026
da6ab1f
Improvement for OpenAPI fix
mchf Mar 13, 2026
30412a4
Cleanup: removed duplicities using serde alias
mchf Mar 13, 2026
1707679
Merge branch 'master' into multiple_ssh_keys
mchf Mar 13, 2026
c938bb8
Open SSH port and activate SSH service even when for first user
mchf Mar 13, 2026
49b0190
Refactoring: parsing of sshPublicKey for user and root
mchf Mar 13, 2026
2f37766
Refactoring: replaced if ! with unless on request :-/
mchf Mar 13, 2026
0180da4
Refactoring: cleaned according to clippy
mchf Mar 13, 2026
cb20375
Temporal adjustment to the system schema
ancorgs Mar 13, 2026
a3d696d
service: Hotfix for wrong report of TpmFde encryption
ancorgs Mar 13, 2026
ef005fc
web: Adapt system openAPI export
ancorgs Mar 13, 2026
700ad4b
Changelogs
ancorgs Mar 13, 2026
e42a209
Fixed rust tests
mchf Mar 14, 2026
a904e95
Updated changelog
mchf Mar 14, 2026
02d8f36
StringOrList for user as is for root user. Second try.
mchf Mar 15, 2026
c7b2907
Merge branch 'master' into multiple_ssh_keys
mchf Mar 15, 2026
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
60 changes: 58 additions & 2 deletions rust/agama-lib/share/profile.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,36 @@
"hashedPassword": {
"title": "Flag for hashed password (true) or plain text password (false or not defined)",
"type": "boolean"
},
"sshPublicKey": {
"title": "One or more SSH keys",
"anyOf": [
{
"type": "string",
"title": "Single SSH Key"
},
{
"type": "array",
"items": { "type": "string" },
"title": "List of SSH Keys",
"minItems": 1
}
]
},
"sshPublicKeys": {
"title": "One or more SSH keys",
"anyOf": [
{
"type": "string",
"title": "Single SSH Key"
},
{
"type": "array",
"items": { "type": "string" },
"title": "List of SSH Keys",
"minItems": 1
}
]
}
},
"required": ["fullName", "userName", "password"]
Expand All @@ -725,8 +755,34 @@
"type": "boolean"
},
"sshPublicKey": {
"title": "SSH public key",
"type": "string"
"title": "One or more SSH keys",
"anyOf": [
{
"type": "string",
"title": "Single SSH Key"
},
{
"type": "array",
"items": { "type": "string" },
"title": "List of SSH Keys",
"minItems": 1
}
]
},
"sshPublicKeys": {
"title": "One or more SSH keys",
"anyOf": [
{
"type": "string",
"title": "Single SSH Key"
},
{
"type": "array",
"items": { "type": "string" },
"title": "List of SSH Keys",
"minItems": 1
}
]
}
}
},
Expand Down
51 changes: 42 additions & 9 deletions rust/agama-users/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,18 @@ impl Model {
)));
}

self.set_user_group(user_name)?;
let ssh_keys = user
.ssh_public_key
.as_ref()
.map(|k| k.to_vec())
.unwrap_or_default();

self.activate_ssh(
&PathBuf::from(format!("/home/{}/.ssh", user_name)),
&ssh_keys,
)?;

let _ = self.set_user_group(user_name);
self.set_user_password(user_name, user_password)?;
self.update_user_fullname(user)
}
Expand All @@ -144,9 +155,24 @@ impl Model {
self.set_user_password("root", root_password)?;
}

// store ssh key for root if any
if let Some(ref root_ssh_key) = root.ssh_public_key {
self.update_authorized_keys(root_ssh_key)?;
// store sshPublicKeys for root if any
let ssh_keys = root
.ssh_public_key
.as_ref()
.map(|k| k.to_vec())
.unwrap_or_default();

self.activate_ssh(&PathBuf::from("root/.ssh/authorized_keys"), &ssh_keys)?;

Ok(())
}

fn activate_ssh(&self, path: &PathBuf, ssh_keys: &[String]) -> Result<(), service::Error> {
if !ssh_keys.is_empty() {
// if some SSH keys were defined
// - update authorized_keys file
// - open SSH port and enable SSH service
self.update_authorized_keys(path, ssh_keys)?;
self.enable_sshd_service()?;
self.open_ssh_port()?;
}
Expand Down Expand Up @@ -210,9 +236,13 @@ impl Model {
}

/// Updates root's authorized_keys file with SSH key
fn update_authorized_keys(&self, ssh_key: &str) -> Result<(), service::Error> {
fn update_authorized_keys(
&self,
keys_path: &PathBuf,
ssh_keys: &[String],
) -> Result<(), service::Error> {
let mode = 0o644;
let file_name = self.install_dir.join("root/.ssh/authorized_keys");
let file_name = self.install_dir.join(keys_path);
let mut authorized_keys_file = OpenOptions::new()
.create(true)
.append(true)
Expand All @@ -223,9 +253,12 @@ impl Model {
// sets mode also for an existing file
fs::set_permissions(&file_name, Permissions::from_mode(mode))?;

writeln!(authorized_keys_file, "{}", ssh_key.trim())?;

Ok(())
ssh_keys
.iter()
.try_for_each(|ssh_key| -> Result<(), service::Error> {
writeln!(authorized_keys_file, "{}", ssh_key.trim())?;
Ok(())
})
}

/// Enables sshd service in the target system
Expand Down
64 changes: 60 additions & 4 deletions rust/agama-utils/src/api/users/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ pub struct FirstUserConfig {
/// First user's username
#[merge(strategy = merge::option::overwrite_none)]
pub user_name: Option<String>,
#[merge(strategy = merge::option::overwrite_none)]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(alias = "ssh_public_keys")]
#[schema(inline)]
pub ssh_public_key: Option<StringOrList>,
}

impl FirstUserConfig {
Expand Down Expand Up @@ -125,6 +130,30 @@ fn overwrite_if_not_empty(old: &mut String, new: String) {
}
}

#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, utoipa::ToSchema)]
#[schema(as = StringOrList)]
#[serde(untagged)]
pub enum StringOrList {
Single(String),
List(Vec<String>),
}

impl StringOrList {
pub fn to_vec(&self) -> Vec<String> {
match self {
StringOrList::Single(s) => vec![s.clone()],
StringOrList::List(v) => v.clone(),
}
}

pub fn is_empty(&self) -> bool {
match self {
StringOrList::Single(s) => s.is_empty(),
StringOrList::List(v) => v.is_empty(),
}
}
}

/// Root user settings
///
/// Holds the settings for the root user.
Expand All @@ -139,7 +168,9 @@ pub struct RootUserConfig {
/// Root SSH public key
#[merge(strategy = merge::option::overwrite_none)]
#[serde(skip_serializing_if = "Option::is_none")]
pub ssh_public_key: Option<String>,
#[serde(alias = "ssh_public_keys")]
#[schema(inline)]
pub ssh_public_key: Option<StringOrList>,
}

impl RootUserConfig {
Expand All @@ -152,7 +183,7 @@ impl RootUserConfig {
return false;
}

if self.ssh_public_key.as_ref().is_some_and(|p| !p.is_empty()) {
if self.ssh_public_key.as_ref().is_some_and(|k| !k.is_empty()) {
return false;
}

Expand All @@ -162,7 +193,7 @@ impl RootUserConfig {

#[cfg(test)]
mod test {
use super::{Config, FirstUserConfig, RootUserConfig, UserPassword};
use super::{Config, FirstUserConfig, RootUserConfig, StringOrList, UserPassword};

#[test]
fn test_parse_user_password() {
Expand Down Expand Up @@ -234,14 +265,24 @@ mod test {
assert!(root_with_empty_password_config.is_empty());

let root_with_ssh_key = RootUserConfig {
ssh_public_key: Some("12345678".to_string()),
ssh_public_key: Some(StringOrList::Single("12345678".to_string())),
..Default::default()
};
let root_with_ssh_key_config = Config {
root: Some(root_with_ssh_key),
..Default::default()
};
assert!(!root_with_ssh_key_config.is_empty());

let root_with_ssh_keys = RootUserConfig {
ssh_public_key: Some(StringOrList::List(vec!["12345678".to_string()])),
..Default::default()
};
let root_with_ssh_keys_config = Config {
root: Some(root_with_ssh_keys),
..Default::default()
};
assert!(!root_with_ssh_keys_config.is_empty());
}

#[test]
Expand All @@ -255,6 +296,7 @@ mod test {
password: "12345678".to_string(),
hashed_password: false,
}),
ssh_public_key: None,
};
assert!(valid_user.is_valid());

Expand All @@ -265,6 +307,7 @@ mod test {
password: "12345678".to_string(),
hashed_password: false,
}),
ssh_public_key: None,
};
assert!(!empty_user_name.is_valid());

Expand All @@ -275,6 +318,7 @@ mod test {
password: "12345678".to_string(),
hashed_password: false,
}),
ssh_public_key: None,
};
assert!(!empty_full_name.is_valid());

Expand All @@ -285,7 +329,19 @@ mod test {
password: "".to_string(),
hashed_password: false,
}),
ssh_public_key: None,
};
assert!(!empty_password.is_valid());

let with_ssh_keys = FirstUserConfig {
user_name: Some("firstuser".to_string()),
ssh_public_key: Some(StringOrList::List(vec!["12345678".to_string()])),
..Default::default()
};
let with_ssh_keys_config = Config {
first_user: Some(with_ssh_keys),
..Default::default()
};
assert!(!with_ssh_keys_config.is_empty());
}
}
9 changes: 9 additions & 0 deletions rust/package/agama.changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
-------------------------------------------------------------------
Sat Mar 14 20:15:07 UTC 2026 - Michal Filka <mfilka@suse.com>

- jsc#PED-15434
- support for multiple SSH keys for root even first user in
agama profile
- both sshPublicKey and sshPublicKeys aliases are available and
handled in the same way

-------------------------------------------------------------------
Fri Mar 13 15:51:57 UTC 2026 - Ancor Gonzalez Sosa <ancor@suse.com>

Expand Down
14 changes: 12 additions & 2 deletions service/lib/agama/autoyast/root_reader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ def read
hsh["hashedPassword"] = true if password.value.encrypted?
end

public_key = root_user.authorized_keys.first
hsh["sshPublicKey"] = public_key if public_key
hsh = hsh.merge(setup_ssh(root_user))

return {} if hsh.empty?

Expand All @@ -66,6 +65,17 @@ def config
result = reader.read
@config = result.config
end

def setup_ssh(root_user)
hsh = {}

public_key = root_user.authorized_keys.first

hsh["sshPublicKey"] = public_key if public_key
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you exporting the sshPublicKey? I would expect to only use sshPublicKeys.

hsh["sshPublicKeys"] = root_user.authorized_keys unless root_user.authorized_keys.empty?

hsh
end
end
end
end
14 changes: 10 additions & 4 deletions service/lib/agama/autoyast/user_reader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,16 @@ def read
user = config.users.find { |u| !u.system? && !u.root? }
return {} unless user

hsh = {
"userName" => user.name,
"fullName" => user.gecos.first.to_s
}
hsh = basic_user_info(user)

password = user.password
if password
hsh["password"] = password.value.to_s
hsh["hashedPassword"] = true if password.value.encrypted?
end

hsh["sshPublicKeys"] = user.authorized_keys unless user.authorized_keys.empty?

{ "user" => hsh }
end

Expand All @@ -63,6 +62,13 @@ def config
result = reader.read
@config = result.config
end

def basic_user_info(user)
{
"userName" => user.name,
"fullName" => user.gecos.first.to_s
}
end
end
end
end
4 changes: 3 additions & 1 deletion service/test/agama/autoyast/root_reader_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@
it "includes a 'root' key with the root user data" do
root = subject.read["root"]
expect(root).to eq(
"password" => "123456", "sshPublicKey" => "ssh-key 1"
"password" => "123456",
"sshPublicKey" => "ssh-key 1",
"sshPublicKeys" => ["ssh-key 1", "ssh-key 2"]
)
end

Expand Down
Loading