From: Daan De Meyer Date: Wed, 24 May 2023 12:33:49 +0000 (+0200) Subject: Firstboot improvements X-Git-Tag: v15~149 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=98f0d17d32c44d73c644b99a81e4fb3bf06270f8;p=thirdparty%2Fmkosi.git Firstboot improvements - Merge --root-password-hashed and --root-password-file into --root-password. If prefixed with hashed:, we treat it as a hashed root password. - When not building an initrd, also store corresponding credentials in /usr/lib/credstore, so that the settings work even if only /usr is shipped in the final image. We don't do this for initrds since those generally ship with /etc populated. - Drop setting of firstboot.hostname which isn't actually used by systemd-firstboot --- diff --git a/mkosi.md b/mkosi.md index 9c67a3315..53417c532 100644 --- a/mkosi.md +++ b/mkosi.md @@ -868,13 +868,19 @@ a boolean argument: either "1", "yes", or "true" to enable, or "0", `Keymap=`, `--keymap=`, `Timezone=`, `--timezone=`, `Hostname=`, `--hostname=`, -`RootPassword=`, `--root-password=`, -`RootPasswordHashed=`, `--root-password-hashed=`, -`RootPasswordFile=`, `--root-password-file=`, `RootShell=`, `--root-shell=` : These settings correspond to the identically named systemd-firstboot options. See the systemd firstboot [manpage](https://www.freedesktop.org/software/systemd/man/systemd-firstboot.html) for more information. + Additionally, where applicable, the corresponding systemd credentials for these settings are written to + `/usr/lib/credstore`, so that they apply even if only `/usr` is shipped in the image. + +`RootPassword=`, `--root-password=`, + +: Set the system root password. If this option is not used, but a `mkosi.rootpw` file is found in the local + directory, the password is automatically read from it. If the password starts with `hashed:`, it is treated + as an already hashed root password. The root password is also stored in `/usr/lib/credstore` under the + appropriate systemd credential so that it applies even if only `/usr` is shipped in the image. ### [Validation] Section @@ -1246,13 +1252,11 @@ local directory: script. After running the build script, the contents of this directory are installed into the final image. This is useful to inspect the results of the install step of the build. -* The **`mkosi.rootpw`** file can be used to provide the password or - hashed password (if `--password-is-hashed` is set) for the root user - of the image. The password may optionally be followed by a newline - character which is implicitly removed. The file must have an access - mode of 0600 or less. If this file does not exist, the - distribution's default root password is set (which usually means - access to the root user is blocked). +* The **`mkosi.rootpw`** file can be used to provide the password for the root user of the image. If the + password is prefixed with `hashed:` it is treated as an already hashed root password. The password may + optionally be followed by a newline character which is implicitly removed. The file must have an access + mode of 0600 or less. If this file does not exist, the distribution's default root password is set (which + usually means access to the root user is blocked). * The **`mkosi.passphrase`** file provides the passphrase to use when LUKS encryption is selected. It should contain the passphrase diff --git a/mkosi/__init__.py b/mkosi/__init__.py index e0073b3f6..b1ee0a44d 100644 --- a/mkosi/__init__.py +++ b/mkosi/__init__.py @@ -751,6 +751,12 @@ def install_unified_kernel(state: MkosiState, roothash: Optional[str]) -> None: # Default values are assigned via the parser so we go via the argument parser to construct # the config for the initrd. with complete_step("Building initrd"): + password, hashed = state.config.root_password or (None, False) + if password: + rootpwopt = f"hashed:{password}" if hashed else password + else: + rootpwopt = None + args, presets = MkosiConfigParser().parse([ "--directory", "", "--distribution", str(state.config.distribution), @@ -782,9 +788,7 @@ def install_unified_kernel(state: MkosiState, roothash: Optional[str]) -> None: *(["--keymap", state.config.keymap] if state.config.keymap else []), *(["--timezone", state.config.timezone] if state.config.timezone else []), *(["--hostname", state.config.hostname] if state.config.hostname else []), - *(["--root-password", state.config.root_password] if state.config.root_password else []), - *(["--root-password-hashed", state.config.root_password_hashed] if state.config.root_password_hashed else []), - *(["--root-password-file", os.fspath(state.config.root_password_file)] if state.config.root_password_file else []), + *(["--root-password", rootpwopt] if rootpwopt else []), *(["-f"] * state.args.force), "build", ]) @@ -1256,7 +1260,7 @@ def summary(args: MkosiArgs, config: MkosiConfig) -> str: Keymap: {none_to_default(config.keymap)} Timezone: {none_to_default(config.timezone)} Hostname: {none_to_default(config.hostname)} - Root Password: {("(set)" if config.root_password or config.root_password_hashed or config.root_password_file else "(default)")} + Root Password: {("(set)" if config.root_password else "(default)")} Root Shell: {none_to_default(config.root_shell)} Autologin: {yes_no(config.autologin)} @@ -1425,32 +1429,49 @@ def run_preset(state: MkosiState) -> None: def run_firstboot(state: MkosiState) -> None: + password, hashed = state.config.root_password or (None, False) + pwopt = "--root-password-hashed" if hashed else "--root-password" + pwcred = "passwd.hashed-password.root" if hashed else "passwd.plaintext-password.root" + settings = ( - ("--locale", state.config.locale), - ("--locale-messages", state.config.locale_messages), - ("--keymap", state.config.keymap), - ("--timezone", state.config.timezone), - ("--hostname", state.config.hostname), - ("--root-password", state.config.root_password), - ("--root-password-hashed", state.config.root_password_hashed), - ("--root-password-file", state.config.root_password_file), - ("--root-shell", state.config.root_shell), + ("--locale", "firstboot.locale", state.config.locale), + ("--locale-messages", "firstboot.locale-messages", state.config.locale_messages), + ("--keymap", "firstboot.keymap", state.config.keymap), + ("--timezone", "firstboot.timezone", state.config.timezone), + ("--hostname", None, state.config.hostname), + (pwopt, pwcred, password), + ("--root-shell", "passwd.shell.root", state.config.root_shell), ) options = [] + creds = [] - for setting, value in settings: + for option, cred, value in settings: if not value: continue - options += [setting, value] + options += [option, value] + + if cred: + creds += [(cred, value)] - if not options: + if not options and not creds: return with complete_step("Applying first boot settings"): run(["systemd-firstboot", "--root", state.root, "--force", *options]) + # Initrds generally don't ship with only /usr so there's not much point in putting the credentials in + # /usr/lib/credstore. + if state.config.output_format != OutputFormat.cpio or not state.config.make_initrd: + (state.root / "usr/lib/credstore").mkdir(mode=0o755, exist_ok=True) + + for cred, value in creds: + (state.root / "usr/lib/credstore" / cred).write_text(value) + + if "password" in cred: + (state.root / "usr/lib/credstore" / cred).chmod(0o600) + def run_selinux_relabel(state: MkosiState) -> None: selinux = state.root / "etc/selinux/config" diff --git a/mkosi/config.py b/mkosi/config.py index 3c34e1d13..1a9779f3d 100644 --- a/mkosi/config.py +++ b/mkosi/config.py @@ -93,7 +93,7 @@ def parse_path(value: str, if absolute: path = path.absolute() - if secret: + if secret and path.exists(): mode = path.stat().st_mode & 0o777 if mode & 0o007: die(textwrap.dedent(f"""\ @@ -444,6 +444,20 @@ def match_path_exists(value: str) -> bool: return Path(value).exists() +def config_parse_root_password(dest: str, value: Optional[str], namespace: argparse.Namespace) -> Optional[tuple[str, bool]]: + if dest in namespace: + return getattr(namespace, dest) # type: ignore + + if not value: + return None + + value = value.strip() + hashed = value.startswith("hashed:") + value = value.removeprefix("hashed:") + + return (value, hashed) + + @dataclasses.dataclass(frozen=True) class MkosiConfigSetting: dest: str @@ -454,6 +468,8 @@ class MkosiConfigSetting: default: Any = None default_factory: Optional[ConfigDefaultCallback] = None paths: tuple[str, ...] = tuple() + path_read_text: bool = False + path_secret: bool = False def __post_init__(self) -> None: if not self.name: @@ -634,9 +650,7 @@ class MkosiConfig: keymap: Optional[str] timezone: Optional[str] hostname: Optional[str] - root_password: Optional[str] - root_password_hashed: Optional[str] - root_password_file: Optional[Path] + root_password: Optional[tuple[str, bool]] root_shell: Optional[str] # QEMU-specific options @@ -1056,18 +1070,10 @@ class MkosiConfigParser: MkosiConfigSetting( dest="root_password", section="Content", - parse=config_parse_string, - ), - MkosiConfigSetting( - dest="root_password_hashed", - section="Content", - parse=config_parse_string, - ), - MkosiConfigSetting( - dest="root_password_file", - section="Content", - parse=config_make_path_parser(secret=True), + parse=config_parse_root_password, paths=("mkosi.rootpw",), + path_read_text=True, + path_secret=True, ), MkosiConfigSetting( dest="root_shell", @@ -1277,8 +1283,10 @@ class MkosiConfigParser: for s in self.SETTINGS: for f in s.paths: - if Path(f).exists(): - setattr(namespace, s.dest, s.parse(s.dest, f, namespace)) + p = parse_path(f, secret=s.path_secret, required=False, absolute=False, expanduser=False, expandvars=False) + if p.exists(): + setattr(namespace, s.dest, + s.parse(s.dest, p.read_text() if s.path_read_text else f, namespace)) return True @@ -1759,18 +1767,6 @@ class MkosiConfigParser: metavar="PASSWORD", action=action, ) - group.add_argument( - "--root-password-hashed", - help="Set the system root password (hashed)", - metavar="PASSWORD-HASHED", - action=action, - ) - group.add_argument( - "--root-password-file", - help="Set the system root password (file)", - metavar="PATH", - action=action, - ) group.add_argument( "--root-shell", help="Set the system root shell", @@ -2119,9 +2115,6 @@ def load_credentials(args: argparse.Namespace) -> dict[str, str]: if "firstboot.locale" not in creds: creds["firstboot.locale"] = "C.UTF-8" - if "firstboot.hostname" not in creds: - creds["firstboot.hostname"] = args.output - if args.ssh and "ssh.authorized_keys.root" not in creds and "SSH_AUTH_SOCK" in os.environ: key = run( ["ssh-add", "-L"],