Skip to content

Add LUKS disk encryption support for Linux OS morphing#436

Open
claudiubelu wants to merge 6 commits into
cloudbase:masterfrom
claudiubelu:luks-os-morphing
Open

Add LUKS disk encryption support for Linux OS morphing#436
claudiubelu wants to merge 6 commits into
cloudbase:masterfrom
claudiubelu:luks-os-morphing

Conversation

@claudiubelu
Copy link
Copy Markdown
Member

The bulk of the work lives in the new LinuxLuksMixin class
(osmount/luks_mixin.py), which is then included in BaseLinuxOSMountTools:

  • mount_os(): check osmorphing_info["disk_luks_passphrases"] for each block device. confirmed LUKS containers are opened via cryptsetup luksOpen and the resulting /dev/mapper/<name> path is used in place of the raw device. dismount_os() closes them again after all filesystems have been unmounted.

  • remove_encryption_artifacts: after OS morphing, stale TPM2 LUKS tokens and their keyslots are killed and the corresponding tpm2-* options are stripped from /etc/crypttab. The source TPM does not exist on the destination, so leaving these in place would cause the initramfs to hang or fail on first boot.

  • install_encryption_firstboot_setup: a temporary migration keyfile is injected into the guest, /etc/crypttab is updated to reference it, the initramfs is rebuilt so the migrated VM can boot, GRUB is patched to use the crypttab mapper names instead of the osmount-time names, and a systemd one-shot service is installed to re-enroll TPM2 and remove the migration keyfile on the first boot of the destination VM.

The firstboot shell scripts live in coriolis/osmorphing/osmount/resources:

  • luks_firstboot_initramfs_tools.sh and targets update-initramfs-based systems (Debian / Ubuntu).
  • luks_firstboot_dracut.sh: targets dracut-based systems.

@claudiubelu claudiubelu force-pushed the luks-os-morphing branch 4 times, most recently from 33a3ef6 to 1e84389 Compare May 22, 2026 11:33
Comment thread coriolis/osmorphing/osmount/resources/luks_firstboot_dracut.sh

def _configure_dracut_keyfiles(self, os_root_dir, uuid_to_keyfile):
"""Write a dracut.conf.d snippet to embed keyfiles in the initramfs."""
for dracut_bin in ["usr/bin/dracut", "usr/sbin/dracut", "sbin/dracut"]:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd just update the caller to use _detect_initramfs_tool and remove this check. The caller would then use either _configure_dracut_keyfiles or _configure_initramfs_tools_keyfiles.


return "sysvinit"

def _register_firstboot_script_systemd(self, os_root_dir):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll also need this for user provided "firstboot" scripts. I guess we'll end up with a separate public method as part of BaseOSMorphingTools.

disk_id = os.path.basename(self._src_device)

# Write a minimal Linux OS on the device, encrypted with LUKS.
test_utils.make_luks_device(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Later on, it might be useful to test the scenario in which there are multiple disk attachments. We'd have to ensure that Coriolis doesn't error out if the user didn't specify the keys for secondary encrypted disks.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, but that is going to be more relevant in more complex tests, in which we actually spin up a replica and assert that the first boot scripts also worked as intended, and then we can also assert that additional disks are also accessible. Not much we can do here, in these tests specifically.

Reads osmorphing_info["disk_luks_passphrases"], containing mappings
{"device_path": "passphrase"}.

For each device that has a passphrase entry and is confirmed LUKS by
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the user will have to specify the luks passphrase for each disk path, right?

Do we expect those to be udev links based on the disk WWN or serial ID? For example:

/dev/disk/by-uuid/4306c488-13c8-447a-825d-3e9216876081
/dev/disk/by-id/wwn-0x6d094660793802002afcbbe61cfbcd38
/dev/disk/by-id/scsi-36d094660793802002afcbbe61cfbcd38

Do we need coriolis-web changes to accommodate this?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed during the call, the user can't know these paths beforehand since the original WWN / SCSI ID won't be preserved.

The consensus was that we're going to use the same key for all LUKS encrypted disks.


_SYSTEMD_UNIT = """\
[Unit]
Description=Coriolis LUKS migration firstboot cleanup
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't it run at every boot, not just the first one?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nevermind, the invoked scripts are supposed to disable the service.

The bulk of the work lives in the new `LinuxLUKSMixin` class
(`osmount/luks_mixin.py`), which is then included in `BaseLinuxOSMountTools`:

- `mount_os()`: check `osmorphing_info["disk_luks_passphrases"]` for each
  block device. confirmed LUKS containers are opened via `cryptsetup luksOpen`
  and the resulting `/dev/mapper/<name>` path is used in place of the raw device.
  `dismount_os()` closes them again after all filesystems have been unmounted.

- `remove_encryption_artifacts`: after OS morphing, stale TPM2 LUKS tokens and
  their keyslots are killed and the corresponding `tpm2-*` options are stripped
  from `/etc/crypttab`. The source TPM does not exist on the destination, so
  leaving these in place would cause the initramfs to hang or fail on first boot.

- `install_encryption_firstboot_setup`: a temporary migration keyfile is injected
  into the guest, `/etc/crypttab` is updated to reference it, the initramfs is
  rebuilt so the migrated VM can boot, GRUB is patched to use the crypttab mapper
  names instead of the osmount-time names, and a systemd one-shot service is
  installed to re-enroll TPM2 and remove the migration keyfile on the first boot
  of the destination VM.

The firstboot shell script itself lives in
`coriolis/osmorphing/osmount/resources/luks_firstboot_initramfs_tools.sh` and
targets `update-initramfs`-based systems (Debian / Ubuntu).
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.
Adds `cryptsetup` to the `data-minion` Dockerfile. Required by osmount
LUKS unlock / lock; `cryptsetup` `luksOpen` / `luksClose` are called
over SSH on the morphing container.

Adds `make_luks_device` to `test_utils.py`: formats the device with LUKS,
opens it, writes a minimal Linux OS tree inside via make_os_device(),
then closes the mapper.

Adds integration test in which the source disk is LUKS-encrypted. The
test runs a full transfer + deployment with skip_os_morphing=False, and
asserts that it completed.

def _prepare_src_device(self):
disk_id = os.path.basename(self._src_device)
os.path.basename(self._src_device)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leftover? It's using os.path.basename without assigning it.


def _prepare_src_device(self):
disk_id = os.path.basename(self._src_device)
os.path.basename(self._src_device)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants