]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
Firstboot improvements
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Wed, 24 May 2023 12:33:49 +0000 (14:33 +0200)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Wed, 24 May 2023 14:59:30 +0000 (16:59 +0200)
- 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

mkosi.md
mkosi/__init__.py
mkosi/config.py

index 9c67a3315c31af5533f6d5b05a6c3e319decd840..53417c532c5613c4d9fb3a8ea5118617c6ae9427 100644 (file)
--- 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
index e0073b3f645b0bc107e6448bd8fb27d6c0402d87..b1ee0a44d0ab3e617aa15fc085b514f9afad052a 100644 (file)
@@ -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"
index 3c34e1d138e448d8d20fe6b086d57a3bd5c09ad0..1a9779f3da555495497da1d8a9866d00d5ee9d98 100644 (file)
@@ -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"],