diff --git a/archinstall/lib/authentication/authentication_menu.py b/archinstall/lib/authentication/authentication_menu.py index 15369212e7..b36e1a1547 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=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], + 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,27 @@ 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? 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, + header=header, + 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) 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" },