Skip to content

Commit fb9ff3a

Browse files
committed
Improves LUKS initramfs rebuild for dracut and chroot environments
- Conditionally add 'initramfs' crypttab option only for update-initramfs systems (not dracut, where it may confuse the parser) - Case-insensitive UUID matching in crypttab keyfile updates - Per-kernel dracut rebuild instead of --regenerate-all for reliable --include handling across distributions - Add --no-hostonly and --add crypt flags for generic initramfs in chroot - Wrap LUKS UUID lookup errors with a descriptive CoriolisException
1 parent 781aab1 commit fb9ff3a

5 files changed

Lines changed: 160 additions & 38 deletions

File tree

coriolis/osmorphing/osmount/base.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,8 @@ def _get_mounted_devices(self):
401401
mounted_device_numbers.append(
402402
self._exec_cmd(dev_nmb_cmd % dev_name).rstrip())
403403

404-
block_devs = self._exec_cmd("ls -al /dev | grep ^b").splitlines()
404+
block_devs = self._exec_cmd(
405+
"ls -al /dev | grep ^b || true").splitlines()
405406
for dev_line in block_devs:
406407
dev = dev_line.split()
407408
major_minor = "%s:%s" % (

coriolis/osmorphing/osmount/luks_mixin.py

Lines changed: 59 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,16 @@ def _get_luks_uuid(self, dev_path):
307307

308308
def _update_crypttab_keyfile(self, os_root_dir, uuid_to_keyfile):
309309
"""Update the keyfile column in crypttab for matching LUKS UUIDs."""
310+
# cryptsetup-initramfs (Ubuntu 22.04+) only embeds crypttab entries
311+
# in the initramfs when the device is verifiable at build time OR when
312+
# the 'initramfs' option is present. Inside a chroot (no udev, no
313+
# /dev/disk/by-uuid/), verification always fails, so we force-add
314+
# 'initramfs' for update-initramfs systems.
315+
# On dracut systems the option is unnecessary and may confuse the
316+
# crypttab parser.
317+
add_initramfs_opt = (
318+
self._detect_initramfs_tool(os_root_dir) == "update-initramfs")
319+
310320
def _set_keyfile(parts):
311321
if len(parts) < 2:
312322
return None
@@ -316,21 +326,16 @@ def _set_keyfile(parts):
316326
if not m:
317327
return None
318328

319-
keyfile = uuid_to_keyfile.get(m.group(1))
329+
keyfile = uuid_to_keyfile.get(m.group(1).lower())
320330
if keyfile is None:
321331
return None
322332

323333
while len(parts) < 4:
324334
parts.append("")
325335

326336
parts[2] = keyfile
327-
# cryptsetup-initramfs (Ubuntu 22.04+) only embeds crypttab
328-
# entries in the initramfs when the device is verifiable at build
329-
# time OR when the 'initramfs' option is present. Inside a chroot
330-
# (no udev, no /dev/disk/by-uuid/), verification always fails, so
331-
# we force-add 'initramfs' here.
332337
opts_list = [o for o in parts[3].split(",") if o]
333-
if "initramfs" not in opts_list:
338+
if add_initramfs_opt and "initramfs" not in opts_list:
334339
opts_list.append("initramfs")
335340

336341
parts[3] = ",".join(opts_list)
@@ -357,7 +362,12 @@ def _write_migration_keyfiles(self, os_root_dir):
357362

358363
uuid_to_keyfile = {}
359364
for _, dev_path in self._luks_opened:
360-
luks_uuid = self._get_luks_uuid(dev_path)
365+
try:
366+
luks_uuid = self._get_luks_uuid(dev_path)
367+
except Exception as ex:
368+
raise exception.CoriolisException(
369+
"Could not determine LUKS UUID for '%s'; "
370+
"cannot write migration keyfile." % dev_path) from ex
361371

362372
keyfile_path = self._get_migration_keyfile_path(dev_path)
363373
abs_path = os.path.join(os_root_dir, keyfile_path.lstrip("/"))
@@ -399,7 +409,16 @@ def _configure_dracut_keyfiles(self, os_root_dir, uuid_to_keyfile):
399409

400410
conf_abs = os.path.join(
401411
os_root_dir, _DRACUT_LUKS_CONF_PATH.lstrip("/"))
402-
conf_content = 'install_items+=" %s "\n' % " ".join(install_items)
412+
# hostonly="no" forces a generic initramfs, so the rebuilt image works
413+
# on the target hypervisor (e.g.: virtio_blk on KVM), regardless of
414+
# what hardware is visible inside the OS morphing minion at build time.
415+
# add_dracutmodules ensures the crypt module is included even when
416+
# dracut's host-detection runs inside a minion without LUKS devices.
417+
conf_content = (
418+
'hostonly="no"\n'
419+
'add_dracutmodules+=" crypt "\n'
420+
'install_items+=" %s "\n'
421+
) % " ".join(install_items)
403422
self._write_remote_file(conf_abs, conf_content)
404423
self._exec_cmd(
405424
"sudo chown root:root %s && sudo chmod 644 %s" % (
@@ -488,18 +507,7 @@ def _rebuild_initramfs(self, os_root_dir):
488507
self._exec_cmd(
489508
"sudo chroot %s update-initramfs -u -k all" % os_root_dir)
490509
elif tool == "dracut":
491-
# --regenerate-all scans the chroot's own /lib/modules/ for
492-
# installed kernels instead of relying on uname -r
493-
#
494-
# Explicitly --include the crypttab and any LUKS migration keyfiles
495-
# so that systemd-cryptsetup-generator finds them in the initramfs
496-
# and uses the crypttab mapper name (luks-root) and keyfile for
497-
# auto-unlock. install_items in dracut.conf.d embeds the keyfile
498-
# but does NOT guarantee that crypttab ends up in the image.
499-
include_args = self._build_dracut_include_args(os_root_dir)
500-
self._exec_cmd(
501-
"sudo chroot %s dracut --regenerate-all --force %s"
502-
% (os_root_dir, " ".join(include_args)))
510+
self._rebuild_initramfs_dracut(os_root_dir)
503511
else:
504512
raise exception.CoriolisException(
505513
"No initramfs tool found in OS at '%s'; cannot rebuild "
@@ -530,6 +538,36 @@ def install_encryption_firstboot_setup(
530538
script_content, user_provided=False,
531539
script_filename="luks-firstboot.sh")
532540

541+
def _rebuild_initramfs_dracut(self, os_root_dir):
542+
"""Rebuild all dracut initramfs images inside the OS chroot."""
543+
include_args = self._build_dracut_include_args(os_root_dir)
544+
545+
try:
546+
kvers_out = self._exec_cmd(
547+
"sudo ls -1 '%s/lib/modules/' 2>/dev/null" % os_root_dir
548+
).strip().splitlines()
549+
kvers = [k.strip() for k in kvers_out if k.strip()]
550+
except Exception:
551+
kvers = []
552+
553+
if not kvers:
554+
raise exception.CoriolisException(
555+
"No kernel versions found under '%s/lib/modules/'; "
556+
"cannot rebuild the initramfs for LUKS auto-unlock." %
557+
os_root_dir)
558+
559+
for kver in kvers:
560+
LOG.info("Rebuilding dracut initramfs for kernel %s", kver)
561+
# --no-hostonly and --add crypt override conf.d settings that
562+
# could be ignored in a chroot without running udevd / uname.
563+
# --add-drivers dm-crypt directly embeds dm-crypt.ko, so the
564+
# crypt DM target type is available even when instmods can't
565+
# resolve it via modules.dep (e.g.: stripped cloud images).
566+
self._exec_cmd(
567+
"sudo chroot '%s' dracut -f --kver '%s' "
568+
"--no-hostonly --add crypt --add-drivers dm-crypt %s"
569+
% (os_root_dir, kver, " ".join(include_args)))
570+
533571
def _fix_grub_luks_root(self, os_root_dir):
534572
"""Patch grub.cfg to use crypttab mapper names for LUKS root devices.
535573

coriolis/osmorphing/osmount/resources/luks_firstboot_dracut.sh

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,11 +144,27 @@ rm -f "${keyfiles[@]}"
144144
rmdir "$KEYFILE_DIR" 2>/dev/null || true
145145

146146
echo "Rebuilding initramfs."
147+
# Rebuild every installed kernel so none of them retain the embedded keyfile.
148+
kvers=()
149+
for kdir in /lib/modules/*/; do
150+
kver="${kdir%/}"
151+
kver="${kver##*/}"
152+
[ -n "$kver" ] && kvers+=("$kver")
153+
done
154+
155+
if [ "${#kvers[@]}" -eq 0 ]; then
156+
echo "ERROR: no kernel versions found under /lib/modules/; cannot rebuild initramfs." >&2
157+
exit 1
158+
fi
159+
147160
# Embed the updated crypttab, so systemd-cryptsetup-generator uses the crypttab
148161
# mapper name and tpm2-device=auto for auto-unlock. The Coriolis dracut.conf.d
149162
# entry is deleted after this rebuild, so its install_items (TPM2 plugin + libtss2)
150163
# are still picked up here.
151-
dracut --force --include /etc/crypttab /etc/crypttab
164+
for kver in "${kvers[@]}"; do
165+
echo "Rebuilding dracut initramfs for kernel $kver."
166+
dracut --force --kver "$kver" --include /etc/crypttab /etc/crypttab
167+
done
152168
rm -f "$DRACUT_CONF"
153169

154170
echo "Firstboot LUKS cleanup complete."

coriolis/tests/integration/test_provider/osmorphing/rocky.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,6 @@ class LUKSTestRockyLinuxOSMorphingTools(TestRockyLinuxOSMorphingTools):
3131
("jq", True),
3232
("dracut", False),
3333
("cryptsetup", False),
34+
("device-mapper", False),
3435
],
3536
}

coriolis/tests/osmorphing/osmount/test_luks_mixin.py

Lines changed: 81 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -340,9 +340,11 @@ def test__get_luks_uuid(self, mock_exec_cmd):
340340
"sudo cryptsetup luksUUID %s" % _DEV
341341
)
342342

343+
@mock.patch.object(luks_mixin.LinuxLUKSMixin, "_detect_initramfs_tool")
343344
@mock.patch.object(luks_mixin.LinuxLUKSMixin, "_transform_crypttab")
344-
def test__update_crypttab_keyfile(self, mock_transform):
345+
def test__update_crypttab_keyfile(self, mock_transform, mock_detect_tool):
345346
mock_transform.return_value = True
347+
mock_detect_tool.return_value = "update-initramfs"
346348
self.mixin._update_crypttab_keyfile(_OS_ROOT_DIR, {_UUID: _KEYFILE})
347349
mock_transform.assert_called_once()
348350

@@ -365,16 +367,32 @@ def test__update_crypttab_keyfile(self, mock_transform):
365367
]
366368
self.assertIsNone(transform(parts))
367369

368-
# UUID= format match; 'initramfs' always appended.
370+
# UUID= format match; update-initramfs -> 'initramfs' option added.
369371
result = transform(["luks-root", "UUID=%s" % _UUID, "none", "none"])
370372
self.assertEqual(result[2], _KEYFILE)
371373
self.assertIn("initramfs", result[3].split(","))
372374

375+
# Case-insensitive UUID match: uppercase UUID in crypttab still matches
376+
# lowercase key in the map.
377+
result = transform(
378+
["luks-root", "UUID=%s" % _UUID.upper(), "none", "none"])
379+
self.assertEqual(result[2], _KEYFILE)
380+
373381
# /by-uuid/ path also matches.
374382
parts = ["luks-root", "/dev/disk/by-uuid/%s" % _UUID, "none", "none"]
375383
result = transform(parts)
376384
self.assertEqual(result[2], _KEYFILE)
377385

386+
# dracut -> 'initramfs' option NOT added.
387+
mock_transform.reset_mock()
388+
mock_detect_tool.return_value = "dracut"
389+
self.mixin._update_crypttab_keyfile(_OS_ROOT_DIR, {_UUID: _KEYFILE})
390+
transform_dracut = mock_transform.call_args[0][1]
391+
result = transform_dracut(
392+
["luks-root", "UUID=%s" % _UUID, "none", "none"])
393+
self.assertEqual(result[2], _KEYFILE)
394+
self.assertNotIn("initramfs", result[3].split(","))
395+
378396
# transform returns False -> exception.
379397
mock_transform.return_value = False
380398
self.assertRaises(
@@ -452,6 +470,16 @@ def test__write_migration_keyfiles(
452470
_OS_ROOT_DIR,
453471
)
454472

473+
# _get_luks_uuid raises: wrapped in CoriolisException.
474+
mock_detect_tool.return_value = "dracut"
475+
mock_uuid.side_effect = Exception("cryptsetup went boom")
476+
self.mixin._luks_opened = [("coriolis_sda", _DEV)]
477+
self.assertRaises(
478+
exception.CoriolisException,
479+
self.mixin._write_migration_keyfiles,
480+
_OS_ROOT_DIR,
481+
)
482+
455483
@mock.patch.object(luks_mixin.LinuxLUKSMixin, "_write_remote_file")
456484
@mock.patch.object(base.BaseSSHOSMountTools, "_exec_cmd")
457485
@mock.patch.object(luks_mixin.utils, "test_ssh_path")
@@ -471,6 +499,8 @@ def test__configure_dracut_keyfiles(
471499
written = mock_write.call_args[0][1]
472500
self.assertIn(_KEYFILE, written)
473501
self.assertIn("install_items+=", written)
502+
self.assertIn('hostonly="no"', written)
503+
self.assertIn('add_dracutmodules+=" crypt "', written)
474504
self.assertNotIn(plugin_path, written)
475505
mock_exec.assert_called_once_with(
476506
"sudo chown root:root %s && sudo chmod 644 %s"
@@ -565,34 +595,27 @@ def test__build_dracut_include_args(self, mock_test, mock_exec):
565595
]
566596
self.assertEqual(result, expected)
567597

568-
@mock.patch.object(luks_mixin.LinuxLUKSMixin, "_build_dracut_include_args")
598+
@mock.patch.object(luks_mixin.LinuxLUKSMixin, "_rebuild_initramfs_dracut")
569599
@mock.patch.object(luks_mixin.LinuxLUKSMixin, "_detect_initramfs_tool")
570600
@mock.patch.object(base.BaseSSHOSMountTools, "_exec_cmd")
571601
def test__rebuild_initramfs(
572-
self, mock_exec, mock_detect, mock_include_args
602+
self, mock_exec, mock_detect, mock_rebuild_dracut
573603
):
574604
# update-initramfs.
575605
mock_detect.return_value = "update-initramfs"
576606
self.mixin._rebuild_initramfs(_OS_ROOT_DIR)
577607
mock_exec.assert_called_once_with(
578608
"sudo chroot %s update-initramfs -u -k all" % _OS_ROOT_DIR
579609
)
610+
mock_rebuild_dracut.assert_not_called()
580611

581-
# dracut: --regenerate-all --force with --include args.
612+
# dracut: delegates to _rebuild_initramfs_dracut.
582613
mock_exec.reset_mock()
583614
mock_detect.return_value = "dracut"
584-
mock_include_args.return_value = [
585-
"--include",
586-
"/etc/crypttab",
587-
"/etc/crypttab",
588-
]
589615
self.mixin._rebuild_initramfs(_OS_ROOT_DIR)
590616

591-
mock_include_args.assert_called_once_with(_OS_ROOT_DIR)
592-
mock_exec.assert_called_once_with(
593-
"sudo chroot %s dracut --regenerate-all --force "
594-
"--include /etc/crypttab /etc/crypttab" % _OS_ROOT_DIR
595-
)
617+
mock_rebuild_dracut.assert_called_once_with(_OS_ROOT_DIR)
618+
mock_exec.assert_not_called()
596619

597620
# no tool found.
598621
mock_detect.return_value = None
@@ -602,6 +625,49 @@ def test__rebuild_initramfs(
602625
_OS_ROOT_DIR,
603626
)
604627

628+
@mock.patch.object(luks_mixin.LinuxLUKSMixin, "_build_dracut_include_args")
629+
@mock.patch.object(base.BaseSSHOSMountTools, "_exec_cmd")
630+
def test__rebuild_initramfs_dracut(self, mock_exec, mock_include_args):
631+
# ls raises: treated as empty kernel list -> CoriolisException.
632+
mock_exec.side_effect = Exception("ls failed")
633+
self.assertRaises(
634+
exception.CoriolisException,
635+
self.mixin._rebuild_initramfs_dracut,
636+
_OS_ROOT_DIR,
637+
)
638+
639+
# ls succeeds but returns empty output -> CoriolisException.
640+
mock_exec.reset_mock()
641+
mock_exec.side_effect = None
642+
mock_exec.return_value = " \n\n"
643+
self.assertRaises(
644+
exception.CoriolisException,
645+
self.mixin._rebuild_initramfs_dracut,
646+
_OS_ROOT_DIR,
647+
)
648+
649+
# initramfs rebuilt.
650+
mock_include_args.return_value = [
651+
"--include",
652+
"/etc/crypttab",
653+
"/etc/crypttab",
654+
]
655+
656+
mock_exec.reset_mock()
657+
mock_exec.side_effect = [
658+
"5.15.0-generic\n", # ls /lib/modules/
659+
None, # dracut call
660+
]
661+
self.mixin._rebuild_initramfs_dracut(_OS_ROOT_DIR)
662+
663+
mock_include_args.assert_called_once_with(_OS_ROOT_DIR)
664+
self.assertEqual(mock_exec.call_count, 2)
665+
mock_exec.assert_any_call(
666+
"sudo chroot '%s' dracut -f --kver '5.15.0-generic' "
667+
"--no-hostonly --add crypt --add-drivers dm-crypt "
668+
"--include /etc/crypttab /etc/crypttab" % _OS_ROOT_DIR
669+
)
670+
605671
@mock.patch.object(luks_mixin.LinuxLUKSMixin, '_detect_initramfs_tool')
606672
@mock.patch.object(luks_mixin.LinuxLUKSMixin, '_rebuild_initramfs')
607673
@mock.patch.object(luks_mixin.LinuxLUKSMixin, '_fix_grub_luks_root')

0 commit comments

Comments
 (0)