From f17b381a3385d2b5aa45a6b4f3bd7db2740cfbd3 Mon Sep 17 00:00:00 2001 From: h8d13 Date: Sun, 4 Jan 2026 15:55:23 +0100 Subject: [PATCH 1/5] Feat: Add lock root option when a sudo user is created Inspired by Fedora/Debian installers --- .../lib/authentication/authentication_menu.py | 46 +++++++++++++++++++ archinstall/lib/installer.py | 10 ++++ archinstall/lib/models/authentication.py | 8 ++++ archinstall/scripts/guided.py | 3 ++ 4 files changed, 67 insertions(+) diff --git a/archinstall/lib/authentication/authentication_menu.py b/archinstall/lib/authentication/authentication_menu.py index 15369212e7..41f9371d6e 100644 --- a/archinstall/lib/authentication/authentication_menu.py +++ b/archinstall/lib/authentication/authentication_menu.py @@ -56,6 +56,14 @@ def _define_menu_options(self) -> list[MenuItem]: preview_action=self._prev_u2f_login, key='u2f_config', ), + MenuItem( + text=tr('Lock root account'), + action=lambda preset: select_lock_root_account(preset), + value=self._auth_config.lock_root_account, + preview_action=self._prev_lock_root, + dependencies=['root_enc_password', self._check_dep_sudo_users], + key='lock_root_account', + ), ] def _create_user_account(self, preset: list[User] | None = None) -> list[User]: @@ -82,6 +90,13 @@ def _depends_on_u2f(self) -> bool: return False return True + def _check_dep_sudo_users(self) -> bool: + """Check if at least one sudo user exists""" + users: list[User] | None = self._item_group.find_by_key('users').value + if users: + return any(user.sudo for user in users) + return False + def _prev_u2f_login(self, item: MenuItem) -> str | None: if item.value is not None: u2f_config: U2FLoginConfiguration = item.value @@ -100,6 +115,11 @@ def _prev_u2f_login(self, item: MenuItem) -> str | None: return None + def _prev_lock_root(self, item: MenuItem) -> str | None: + if item.value is True: + return tr('Root account will be locked after installation') + return None + def select_root_password(preset: str | None = None) -> Password | None: password = get_password(text=tr('Root password'), allow_skip=True) @@ -157,3 +177,29 @@ def select_u2f_login(preset: U2FLoginConfiguration) -> U2FLoginConfiguration | N return None case _: raise ValueError('Unhandled result type') + + +def select_lock_root_account(preset: bool) -> bool: + group = MenuItemGroup.yes_no() + group.focus_item = MenuItem.yes() if preset else MenuItem.no() + + header = tr('Lock root account?') + subheader = tr('Sudo users can still edit /etc/shadow or use sudo directly."') + + result = SelectMenu[bool]( + group, + header=header, + sub_header=subheader, + alignment=Alignment.CENTER, + columns=2, + orientation=Orientation.HORIZONTAL, + allow_skip=True, + ).run() + + match result.type_: + case ResultType.Selection: + return result.item() == MenuItem.yes() + case ResultType.Skip: + return preset + case _: + return False diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index da1941834e..2c7de48a6b 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -1927,6 +1927,16 @@ def set_user_password(self, user: User) -> bool: debug(f'Error setting user password: {err}') return False + def lock_root_account(self) -> bool: + info('Locking root account') + + try: + self.arch_chroot('passwd -l root') + return True + except SysCallError as err: + error(f'Failed to lock root account: {err}') + return False + def user_set_shell(self, user: str, shell: str) -> bool: info(f'Setting shell for {user} to {shell}') diff --git a/archinstall/lib/models/authentication.py b/archinstall/lib/models/authentication.py index 66c777860f..fdd5e5e655 100644 --- a/archinstall/lib/models/authentication.py +++ b/archinstall/lib/models/authentication.py @@ -13,6 +13,7 @@ class U2FLoginConfigSerialization(TypedDict): class AuthenticationSerialization(TypedDict): u2f_config: NotRequired[U2FLoginConfigSerialization] + lock_root_account: NotRequired[bool] class U2FLoginMethod(Enum): @@ -62,6 +63,7 @@ class AuthenticationConfiguration: root_enc_password: Password | None = None users: list[User] = field(default_factory=list) u2f_config: U2FLoginConfiguration | None = None + lock_root_account: bool = False @staticmethod def parse_arg(args: dict[str, Any]) -> 'AuthenticationConfiguration': @@ -73,6 +75,9 @@ def parse_arg(args: dict[str, Any]) -> 'AuthenticationConfiguration': if enc_password := args.get('root_enc_password'): auth_config.root_enc_password = Password(enc_password=enc_password) + if lock_root := args.get('lock_root_account'): + auth_config.lock_root_account = lock_root + return auth_config def json(self) -> AuthenticationSerialization: @@ -81,4 +86,7 @@ def json(self) -> AuthenticationSerialization: if self.u2f_config: config['u2f_config'] = self.u2f_config.json() + if self.lock_root_account: + config['lock_root_account'] = self.lock_root_account + return config diff --git a/archinstall/scripts/guided.py b/archinstall/scripts/guided.py index 70e6cb89ab..a03d9d43cf 100644 --- a/archinstall/scripts/guided.py +++ b/archinstall/scripts/guided.py @@ -144,6 +144,9 @@ def perform_installation(mountpoint: Path) -> None: root_user = User('root', config.auth_config.root_enc_password, False) installation.set_user_password(root_user) + if config.auth_config.lock_root_account: + installation.lock_root_account() + if (profile_config := config.profile_config) and profile_config.profile: profile_config.profile.post_install(installation) From 3d6e45e2d22999a3f1d040e22dd191c3c9317045 Mon Sep 17 00:00:00 2001 From: h8d13 Date: Sun, 4 Jan 2026 16:09:55 +0100 Subject: [PATCH 2/5] Fix headers. --- archinstall/lib/authentication/authentication_menu.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/archinstall/lib/authentication/authentication_menu.py b/archinstall/lib/authentication/authentication_menu.py index 41f9371d6e..d90bfde913 100644 --- a/archinstall/lib/authentication/authentication_menu.py +++ b/archinstall/lib/authentication/authentication_menu.py @@ -183,13 +183,11 @@ def select_lock_root_account(preset: bool) -> bool: group = MenuItemGroup.yes_no() group.focus_item = MenuItem.yes() if preset else MenuItem.no() - header = tr('Lock root account?') - subheader = tr('Sudo users can still edit /etc/shadow or use sudo directly."') + header = tr('Lock root account?\n') + tr('Sudo users can still edit /etc/shadow or use sudo directly.\n') result = SelectMenu[bool]( group, header=header, - sub_header=subheader, alignment=Alignment.CENTER, columns=2, orientation=Orientation.HORIZONTAL, From 00f260f1f114da9804e3e3bf60e7985c5de9d88d Mon Sep 17 00:00:00 2001 From: h8d13 Date: Sun, 4 Jan 2026 16:10:45 +0100 Subject: [PATCH 3/5] More helpful text --- archinstall/lib/authentication/authentication_menu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/archinstall/lib/authentication/authentication_menu.py b/archinstall/lib/authentication/authentication_menu.py index d90bfde913..ac4b478016 100644 --- a/archinstall/lib/authentication/authentication_menu.py +++ b/archinstall/lib/authentication/authentication_menu.py @@ -183,7 +183,7 @@ def select_lock_root_account(preset: bool) -> bool: group = MenuItemGroup.yes_no() group.focus_item = MenuItem.yes() if preset else MenuItem.no() - header = tr('Lock root account?\n') + tr('Sudo users can still edit /etc/shadow or use sudo directly.\n') + header = tr('Lock root account? Can be undone using passwd -u root later.\n') + tr('Sudo users can still edit /etc/shadow or use sudo directly.\n') result = SelectMenu[bool]( group, From de2a0495fff1cbfbb6e1d40fe3441864688aec77 Mon Sep 17 00:00:00 2001 From: h8d13 Date: Sun, 4 Jan 2026 16:18:08 +0100 Subject: [PATCH 4/5] Pylint --- archinstall/lib/authentication/authentication_menu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/archinstall/lib/authentication/authentication_menu.py b/archinstall/lib/authentication/authentication_menu.py index ac4b478016..b36e1a1547 100644 --- a/archinstall/lib/authentication/authentication_menu.py +++ b/archinstall/lib/authentication/authentication_menu.py @@ -58,7 +58,7 @@ def _define_menu_options(self) -> list[MenuItem]: ), MenuItem( text=tr('Lock root account'), - action=lambda preset: select_lock_root_account(preset), + action=select_lock_root_account, value=self._auth_config.lock_root_account, preview_action=self._prev_lock_root, dependencies=['root_enc_password', self._check_dep_sudo_users], From c831ce29fe467bd7eab6cfd888ba27ef00e8343a Mon Sep 17 00:00:00 2001 From: h8d13 Date: Sun, 4 Jan 2026 16:40:20 +0100 Subject: [PATCH 5/5] Update config for new lock root option --- examples/config-sample.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/examples/config-sample.json b/examples/config-sample.json index bc4cd2d924..9b6a02223d 100644 --- a/examples/config-sample.json +++ b/examples/config-sample.json @@ -1,5 +1,8 @@ { "archinstall-language": "English", + "auth_config": { + "lock_root_account": false + }, "audio_config": { "audio": "pipewire" },