Преглед изворни кода

Add LUKS disk encryption support for RHEL / dracut OS morphing

Extends `LinuxLuksMixin` with dracut support for RHEL / Fedora / SUSE guests:

- `_configure_dracut_keyfiles`: writes a `dracut.conf.d/99-coriolis-luks.conf`
  snippet that adds the migration keyfiles to `install_items`, ensuring dracut
  embeds them in the initramfs. It also probes for the
  `libcryptsetup-token-systemd-tpm2.so` plugin (checked against a list of
  known paths) and adds it explicitly, because cryptsetup loads TPM2 token
  plugins via `dlopen` and dracut's `ldd` analysis would otherwise miss it
  along with its `libtss2` dependencies.

- `_build_dracut_include_args`: returns `--include` args that force-embed
  `/etc/crypttab` and all `coriolis_*.key` keyfiles into the initramfs image.
  Without an explicit crypttab embed, dracut names the mapper `luks-<UUID>`
  rather than the crypttab name and cannot find the keyfile at boot.

- `luks_firstboot_dracut.sh`: the firstboot shell script for dracut-based systems.
  Runs once on first boot to re-enroll TPM2, remove the migration keyslots, and
  rebuild the initramfs so the embedded keyfile no longer ships in future initramfs
  images.
Claudiu Belu пре 1 месец
родитељ
комит
9d33b9b7af

+ 89 - 1
coriolis/osmorphing/osmount/luks_mixin.py

@@ -15,6 +15,17 @@ from coriolis import utils
 LOG = logging.getLogger(__name__)
 
 _LUKS_KEYFILE_DIR = "/etc/luks"
+_DRACUT_LUKS_CONF_PATH = "/etc/dracut.conf.d/99-coriolis-luks.conf"
+
+# cryptsetup loads TPM2 token plugins via dlopen, so dracut's ldd analysis
+# misses them. List candidate paths in order of preference; the first one
+# found in the guest OS will be added to install_items so dracut includes
+# both the plugin and its libtss2 dependencies.
+_CRYPTSETUP_TPM2_PLUGIN_PATHS = [
+    "/usr/lib64/cryptsetup/libcryptsetup-token-systemd-tpm2.so",
+    "/usr/lib/cryptsetup/libcryptsetup-token-systemd-tpm2.so",
+    "/usr/lib/x86_64-linux-gnu/cryptsetup/libcryptsetup-token-systemd-tpm2.so",
+]
 
 _FIRSTBOOT_SCRIPT_PATH = "/usr/local/sbin/coriolis-luks-firstboot.sh"
 _SYSTEMD_UNIT_PATH = "/etc/systemd/system/coriolis-luks-firstboot.service"
@@ -29,6 +40,7 @@ def _load_script(filename):
 
 _LUKS_FIRSTBOOT_SCRIPTS = {
     "update-initramfs": _load_script("luks_firstboot_initramfs_tools.sh"),
+    "dracut": _load_script("luks_firstboot_dracut.sh"),
 }
 
 _SYSTEMD_UNIT = """\
@@ -379,13 +391,43 @@ class LinuxLUKSMixin:
         self._update_crypttab_keyfile(os_root_dir, uuid_to_keyfile)
 
         initramfs_tool = self._detect_initramfs_tool(os_root_dir)
-        if initramfs_tool == "update-initramfs":
+        if initramfs_tool == "dracut":
+            self._configure_dracut_keyfiles(os_root_dir, uuid_to_keyfile)
+        elif initramfs_tool == "update-initramfs":
             self._configure_initramfs_tools_keyfiles(os_root_dir)
         else:
             raise exception.CoriolisException(
                 "No initramfs tool found in OS at '%s'; cannot configure "
                 "keyfile-based LUKS auto-unlock." % os_root_dir)
 
+    def _configure_dracut_keyfiles(self, os_root_dir, uuid_to_keyfile):
+        """Write a dracut.conf.d snippet to embed keyfiles in the initramfs."""
+        install_items = list(uuid_to_keyfile.values())
+
+        # cryptsetup loads TPM2 token plugins via dlopen; add it
+        # explicitly so dracut includes it (and its libtss2 deps via
+        # ldd analysis of the .so) in the initramfs.
+        for plugin_path in _CRYPTSETUP_TPM2_PLUGIN_PATHS:
+            if utils.test_ssh_path(
+                    self._ssh,
+                    os.path.join(os_root_dir, plugin_path.lstrip("/"))):
+                install_items.append(plugin_path)
+                LOG.debug(
+                    "Including cryptsetup TPM2 token plugin in "
+                    "dracut install_items: %s", plugin_path)
+                break
+
+        conf_abs = os.path.join(
+            os_root_dir, _DRACUT_LUKS_CONF_PATH.lstrip("/"))
+        conf_content = 'install_items+=" %s "\n' % " ".join(install_items)
+        self._write_remote_file(conf_abs, conf_content)
+        self._exec_cmd(
+            "sudo chown root:root %s && sudo chmod 644 %s" % (
+                conf_abs, conf_abs))
+        LOG.info(
+            "Written dracut LUKS keyfile config at '%s'",
+            _DRACUT_LUKS_CONF_PATH)
+
     def _configure_initramfs_tools_keyfiles(self, os_root_dir):
         """Set KEYFILE_PATTERN in cryptsetup-initramfs conf-hook.
 
@@ -421,8 +463,41 @@ class LinuxLUKSMixin:
             if utils.test_ssh_path(self._ssh, path):
                 return "update-initramfs"
 
+        for dracut_bin in ["usr/bin/dracut", "usr/sbin/dracut", "sbin/dracut"]:
+            path = os.path.join(os_root_dir, dracut_bin)
+            if utils.test_ssh_path(self._ssh, path):
+                return "dracut"
+
         return None
 
+    def _build_dracut_include_args(self, os_root_dir):
+        """Return --include args that force-embed crypttab and LUKS keyfiles.
+
+        dracut's install_items embeds the keyfile but does not guarantee that
+        /etc/crypttab lands in the initramfs image. Without crypttab,
+        systemd-cryptsetup-generator names the mapper luks-<UUID> instead of
+        the crypttab mapper name and cannot find the keyfile.
+        """
+        args = []
+        crypttab = os.path.join(os_root_dir, "etc/crypttab")
+        if utils.test_ssh_path(self._ssh, crypttab):
+            args += ["--include", "/etc/crypttab", "/etc/crypttab"]
+
+        luks_dir = os.path.join(os_root_dir, _LUKS_KEYFILE_DIR.lstrip("/"))
+        try:
+            keyfiles = self._exec_cmd(
+                "sudo find %s -name 'coriolis_*.key' -type f 2>/dev/null"
+                % luks_dir).strip().splitlines()
+        except Exception:
+            keyfiles = []
+
+        for kf in keyfiles:
+            # strip os_root_dir prefix.
+            rel = kf[len(os_root_dir):]
+            args += ["--include", rel, rel]
+
+        return args
+
     def _rebuild_initramfs(self, os_root_dir):
         """Rebuild the initramfs inside the mounted OS chroot.
 
@@ -432,6 +507,19 @@ class LinuxLUKSMixin:
         if tool == "update-initramfs":
             self._exec_cmd(
                 "sudo chroot %s update-initramfs -u -k all" % os_root_dir)
+        elif tool == "dracut":
+            # --regenerate-all scans the chroot's own /lib/modules/ for
+            # installed kernels instead of relying on uname -r
+            #
+            # Explicitly --include the crypttab and any LUKS migration keyfiles
+            # so that systemd-cryptsetup-generator finds them in the initramfs
+            # and uses the crypttab mapper name (luks-root) and keyfile for
+            # auto-unlock. install_items in dracut.conf.d embeds the keyfile
+            # but does NOT guarantee that crypttab ends up in the image.
+            include_args = self._build_dracut_include_args(os_root_dir)
+            self._exec_cmd(
+                "sudo chroot %s dracut --regenerate-all --force %s"
+                % (os_root_dir, " ".join(include_args)))
         else:
             raise exception.CoriolisException(
                 "No initramfs tool found in OS at '%s'; cannot rebuild "

+ 167 - 0
coriolis/osmorphing/osmount/resources/luks_firstboot_dracut.sh

@@ -0,0 +1,167 @@
+#!/bin/bash
+
+# Coriolis LUKS firstboot cleanup for dracut (RHEL / Fedora / SUSE).
+# Runs once on first boot to re-enroll TPM2, remove migration keyslots, and
+# rebuild the initramfs so the embedded keyfile is gone.
+
+set -e
+set -x
+KEYFILE_DIR=/etc/luks
+DRACUT_CONF=/etc/dracut.conf.d/99-coriolis-luks.conf
+
+# helpers
+
+load_keyfile_map() {
+    [ -f /etc/crypttab ] || return 0
+
+    # Format: <target name> <source device> <key file> <options>
+    while read -r _name dev_spec keyfile _; do
+        [[ "$keyfile" == "$KEYFILE_DIR/coriolis_"*.key ]] || continue
+
+        local dev
+        if [[ "$dev_spec" == UUID=* ]]; then
+            dev=$(blkid -l -t "$dev_spec" -o device 2>/dev/null)
+        else
+            dev="$dev_spec"
+        fi
+
+        [ -n "$dev" ] || continue
+
+        # Add mappings.
+        dev_to_keyfile["$dev"]="$keyfile"
+        dev_to_spec["$dev"]="$dev_spec"
+    done < <(grep -Ev '^\s*(#|$)' /etc/crypttab)
+}
+
+wait_for_tpm2() {
+    for dev in /dev/tpmrm0 /dev/tpm0; do
+        local deadline=$(( $(date +%s) + 10 ))
+        until [ -e "$dev" ] || [ "$(date +%s)" -ge "$deadline" ]; do
+            sleep 1
+        done
+
+        if [ -e "$dev" ]; then
+            echo "$dev"
+            return
+        fi
+    done
+}
+
+add_tpm2_crypttab_option() {
+    local dev="$1"
+    local dev_spec="${dev_to_spec[$dev]}"
+
+    # Append ",tpm2-device=auto" to the options field of the matching entry;
+    # all other lines are unchanged.
+    awk -v spec="$dev_spec" '
+        NF >= 4 && $2 == spec && $4 !~ /tpm2-device/ { $4 = $4 ",tpm2-device=auto" }
+        { print }
+    ' OFS='\t' /etc/crypttab > /tmp/.coriolis.crypttab.new
+    mv /tmp/.coriolis.crypttab.new /etc/crypttab
+
+    echo "Added tpm2-device=auto to crypttab for $dev."
+}
+
+enroll_systemd_cryptenroll() {
+    local tpm2_dev
+
+    tpm2_dev=$(wait_for_tpm2)
+    if [ -z "$tpm2_dev" ]; then
+        echo "ERROR: no TPM2 device found; aborting to avoid lockout." >&2
+        return 1
+    fi
+
+    echo "TPM2 device detected ($tpm2_dev); enrolling via systemd-cryptenroll."
+
+    for dev in "${!dev_to_keyfile[@]}"; do
+        local keyfile="${dev_to_keyfile[$dev]}"
+
+        if ! systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs= \
+                --unlock-key-file="$keyfile" "$dev" 2>/dev/null; then
+            echo "ERROR: systemd-cryptenroll failed for $dev; aborting to avoid lockout." >&2
+            return 1
+        fi
+
+        if ! cryptsetup luksDump "$dev" 2>/dev/null | grep -q 'systemd-tpm2'; then
+            echo "ERROR: systemd-tpm2 token not found in LUKS header for $dev; aborting to avoid lockout." >&2
+            return 1
+        fi
+
+        echo "systemd-cryptenroll TPM2 enrollment verified for $dev."
+    done
+}
+
+remove_migration_keyslots() {
+    for dev in "${!dev_to_keyfile[@]}"; do
+        local keyfile="${dev_to_keyfile[$dev]}"
+
+        echo "Removing migration keyslot from $dev using $keyfile."
+        if ! cryptsetup luksRemoveKey "$dev" "$keyfile"; then
+            echo "ERROR: failed to remove migration keyslot from $dev." >&2
+            return 1
+        fi
+
+        echo "Migration keyslot removed from $dev."
+
+        sed -i "s|$keyfile|none|g" /etc/crypttab
+        [ "$tpm2_enrolled" = "1" ] && add_tpm2_crypttab_option "$dev"
+    done
+}
+
+deregister_service() {
+    # Only disable, do NOT delete the unit file, or daemon-reload while the
+    # service is still running. systemd would detect "Current command vanished"
+    # and kill this process immediately.
+    systemctl disable coriolis-luks-firstboot.service 2>/dev/null || true
+}
+
+# main
+
+shopt -s nullglob
+keyfiles=("$KEYFILE_DIR"/coriolis_*.key)
+shopt -u nullglob
+
+if [ "${#keyfiles[@]}" -eq 0 ]; then
+    echo "ERROR: no migration keyfiles found in $KEYFILE_DIR; setup is broken." >&2
+    exit 1
+fi
+
+echo "Found ${#keyfiles[@]} migration keyfile(s): ${keyfiles[*]}"
+
+declare -A dev_to_keyfile dev_to_spec
+load_keyfile_map
+
+if [ "${#dev_to_keyfile[@]}" -eq 0 ]; then
+    echo "ERROR: no coriolis migration entries found in /etc/crypttab; setup is broken." >&2
+    exit 1
+fi
+
+echo "Found ${#dev_to_keyfile[@]} crypttab entry / entries to process."
+
+tpm2_enrolled=0
+if command -v systemd-cryptenroll >/dev/null 2>&1; then
+    enroll_systemd_cryptenroll
+    tpm2_enrolled=1
+fi
+
+remove_migration_keyslots
+
+echo "Deleting migration keyfiles."
+rm -f "${keyfiles[@]}"
+rmdir "$KEYFILE_DIR" 2>/dev/null || true
+
+echo "Rebuilding initramfs."
+# Embed the updated crypttab, so systemd-cryptsetup-generator uses the crypttab
+# mapper name and tpm2-device=auto for auto-unlock. The Coriolis dracut.conf.d
+# entry is deleted after this rebuild, so its install_items (TPM2 plugin + libtss2)
+# are still picked up here.
+dracut --force --include /etc/crypttab /etc/crypttab
+rm -f "$DRACUT_CONF"
+
+deregister_service
+
+echo "Firstboot LUKS cleanup complete."
+rm -f "$0"
+
+echo "Rebooting to finish LUKS first boot cleanup."
+reboot