Skip to content

Latest commit

 

History

History
400 lines (282 loc) · 19 KB

File metadata and controls

400 lines (282 loc) · 19 KB

A Guide to encrypted Dual Boot: Installing Ubuntu 24.04 Noble with encrypted root and swap partitions alongside Windows 11

My goal was to install Ubuntu 24.04 LTS onto a dedicated drive with encrypted root and swap partitions, but since switching to the the latest installer of Ubuntu 24.04, this setup is not possible anymore. Currently, If you want encryption, you can either choose to

  • "Install alongside" Windows, which relies on resizing the Windows partition, a process that is only possible on a decrypted volume, or
  • "Erase disk" and use the whole disk for Ubuntu, without any custom partitioning options.

The "Manual partitioning" option, which would be the logical choice, is non-operational for encrypted volumes due to a known bug. The installer fails to detect LVM volumes residing within a LUKS container.

This is a step back from previous versions and has been persistent since about two years, so it is probably called a feature by now ;) Options suggested elsewhere are:

  • installing onto a separate partition and then copying the root fs, or
  • installing an earlier verson and then upgrading.

But this just feels wrong. After several attempts I decided to do it using debootstrap. This method bypasses the graphical installer entirely by downloading and installing Ubuntu directly.

It also provides the necessary controls to create a precise partition scheme, including encrypted LVM, without altering the existing Windows installation. And since sharing is caring I might be able to help someone else with it. This guide defaults to a Secure Boot compatible installation, as that is the standard for most modern hardware. However, if you have Secure Boot disabled in your UEFI/BIOS and do not intend to use it, I marked the necessary changes in sections 4d and 4f.

A key consideration in this process is the EFI partition. We create a new, separate EFI partition for Ubuntu, which ensures the bootloaders are isolated. Maybe you have a different partitioning scheme in mind, so you will have to tune the commands or leave some of them out.

  1. Manual partitioning
  2. Use debootstrap to install ubuntu
  3. Configure in chroot environment

Also, I am using a German setup. For other configurations, you will likely need to modify:

  • The APT mirror (de.archive.ubuntu.com) for your region.
  • The system locale (de_DE.UTF-8) for your language and formats.
  • The keyboard layout (configured interactively). More on that later.

Prerequisites

Also note that any modification to the boot configuration, including installing a bootloader to the Windows EFI partition, will be detected by the system's Trusted Platform Module (TPM) and will trigger BitLocker's recovery mode, so you must backup your bitlocker recovery key and all your data, or just do it on a fresh install only in case something goes wrong.

WARNING - PLEASE BACKUP ALL YOUR DATA FIRST!!

  1. Backup your bitlocker recovery key
  2. Create a bootable Ubuntu Noble live usb (I assume you have this and failed at manual partitioning to come read this)
  3. Boot the live usb and continue the installation until you have selected language, keyboard layout and configured your internet connection. Then close the installer.
  4. Identify Your Drives and Set Variables:

Open a terminal and run sudo lsblk -f. Use the size and FSTYPE (look for BitLocker or ntfs) to be 100% certain which drive is which. Then, set the following shell variables to prevent typos later. All subsequent commands will use these variables.

# Set this to the drive you will install Ubuntu on.
export UBUNTU_DRIVE="/dev/nvme1n1"

# Set this to your existing Windows drive.
export WINDOWS_DRIVE="/dev/nvme0n1"

Step 1: Manual Partitioning

With the prep work done, it's time to partition the drive. The following commands will be run from the terminal in the live session.

First, open a terminal and switch to root so you don't have to type sudo for every command.

sudo su

The next commands are destructive. wipefs will destroy the partition table on the drive you specify. Because we set the UBUNTU_DRIVE variable, we can be confident we are targeting the correct disk.

We'll wipe the drive and then create a new GPT partition table.

wipefs -a ${UBUNTU_DRIVE}
parted ${UBUNTU_DRIVE} mklabel gpt

Now, we will create the four partitions for our setup. This single block of commands will lay out the entire disk structure.

# EFI Partition (p1)
parted ${UBUNTU_DRIVE} mkpart ESP fat32 1MiB 1024MiB
parted ${UBUNTU_DRIVE} set 1 esp on

# /boot Partition (p2)
parted ${UBUNTU_DRIVE} mkpart primary ext4 1024MiB 3072MiB

# LUKS Container Partition (p3)
parted ${UBUNTU_DRIVE} mkpart primary 3072MiB 420GiB

# Shared Data Partition (p4)
parted ${UBUNTU_DRIVE} mkpart primary 420GiB 100%

Here is a breakdown of what these partitions are for:

  • EFI Partition (p1): A 1023 MiB partition formatted with FAT32. This is mandatory for a UEFI system and parted ... set 1 esp on assigns partition 1 as the EFI System Partition, which will store our GRUB bootloader. Since I am using separate drives for Windows and Ubuntu, I also wanted to have separate EFI partitions with the respective bootloaders on each drive. Boot priority must be configured in UEFI to boot the Ubuntu drive. Then from GRUB we can choose to either boot Ubuntu or Windows. When Windows is selected in GRUB, the bootloader from the EFI partition on the Windows drive is loaded next. This way, if I choose to remove one drive in the future I am still able to boot the remaining system.
  • /boot Partition (p2): A 2 GiB partition. This partition will remain unencrypted because the bootloader needs to be able to read the Linux kernel and initramfs files from here before the main system is decrypted.
  • LUKS Container (p3): A 417 GiB partition. This is just an empty container for now. We will encrypt this partition in the next phase and create our root and swap volumes inside it.
  • Shared Data (p4): This partition uses the rest of the disk space. I will format it with NTFS so it can be easily read and written to by both Windows and Ubuntu.

A note on the partition scheme: 417 GiB is the size I chose for my Ubuntu system. If you do not want a shared partition and wish to use the entire drive for Ubuntu, replace the last two parted commands above with this single command: parted ${UBUNTU_DRIVE} mkpart primary 3072MiB 100%.

Before we format, let's verify the layout. Run sudo parted ${UBUNTU_DRIVE} print. This should show a clean table with the four partitions we just created.

With the partitions confirmed, the final step is to format them.

mkfs.fat -F32 ${UBUNTU_DRIVE}p1
mkfs.ext4 ${UBUNTU_DRIVE}p2

# Format the shared partition (p4). If you chose not to create one, skip this command.
mkfs.ntfs -f ${UBUNTU_DRIVE}p4

Step 2: Encryption and Logical Volumes

With the physical partitions laid out, the next step is to build the encrypted "vault" for our Ubuntu system. We'll be using LUKS for encryption and LVM for flexible volume management inside that encrypted container.

First, we'll format the third partition (p3) as a LUKS encrypted container. This is an interactive step. It will ask you to confirm by typing YES in uppercase, and then you'll set your encryption password. This is your master key. Choose a strong one and do not forget it. There is no recovery.

cryptsetup luksFormat --sector-size=4096 ${UBUNTU_DRIVE}p3

Now for a critical safety step that many guides leave out. We're going to back up the LUKS header. If this small piece of data on the drive ever gets corrupted, your data is gone forever, even with the password.

echo "--- IMPORTANT: Backing up LUKS header ---"
cryptsetup luksHeaderBackup ${UBUNTU_DRIVE}p3 --header-backup-file /home/ubuntu/luks-header-${UBUNTU_DRIVE##*/}-p3.bak
echo "--- LUKS header backup saved to the live USB desktop. Copy this file to an external USB stick before rebooting! ---"

(Note: The ${UBUNTU_DRIVE##*/} part is just a bit of shell magic to strip the /dev/ prefix for a cleaner filename, like luks-header-nvme1n1-p3.bak)

With the container created and backed up, we'll open it. This decrypts it and makes it available as a virtual block device at /dev/mapper/crypt. You'll be prompted for the password you just set.

cryptsetup open ${UBUNTU_DRIVE}p3 crypt

Now, we'll build the LVM structure inside this unlocked container. We're creating a Physical Volume, then a Volume Group called ubuntu-vg, and finally a Logical Volume for the root (/) filesystem that uses all the available space for now.

pvcreate /dev/mapper/crypt
vgcreate ubuntu-vg /dev/mapper/crypt
lvcreate -l 100%FREE -n root ubuntu-vg

The final step is to format the root volume and then carve out space for our 16 GiB swap volume. I'm doing it in this order (create, format, resize) as it's a reliable method.

mkfs.ext4 /dev/ubuntu-vg/root
lvresize --resizefs -L -16G /dev/ubuntu-vg/root
lvcreate -L 16G -n swap ubuntu-vg
mkswap /dev/ubuntu-vg/swap

And that's it for this phase. We now have a fully encrypted container with logical volumes for our root filesystem and swap space, ready for the Ubuntu installation.

Step 3: Base System Installation

With the encrypted LVM ready, it's time to install the base system using debootstrap.

First, we'll mount the new system's filesystems into a temporary structure under /mnt.

# Mount the root, boot, and EFI filesystems
mount /dev/ubuntu-vg/root /mnt
mkdir /mnt/boot
mount ${UBUNTU_DRIVE}p2 /mnt/boot
mkdir /mnt/boot/efi
mount ${UBUNTU_DRIVE}p1 /mnt/boot/efi

Now, we'll install the debootstrap tool and use it to download the core Ubuntu system into /mnt. This will take some time depending on your internet connection.

apt update
apt install -y debootstrap
debootstrap noble /mnt

When it's done, a minimal Ubuntu system will be installed at /mnt, ready for the next phase of configuration.

Step 4: Configuring the New System (The chroot Phase)

The base system is on the disk, but it's not a bootable, usable OS yet. To configure it, we need to "enter" it using a command called chroot. This makes the /mnt directory temporarily become the root (/) of our terminal session, allowing us to run commands as if we were already booted into the new installation.

4a. Preparing and Entering the chroot Environment

Before we can chroot, we need to make the live session's view of the running kernel and its devices available inside the new system. This is done by "bind mounting" several pseudo-filesystems. We also need to copy over the DNS configuration so we can access the internet from inside the chroot.

# Copy DNS info for internet access
cp /etc/resolv.conf /mnt/etc/

# Bind mount the necessary kernel and device filesystems
mount --bind /dev /mnt/dev
mount --bind /proc /mnt/proc
mount --bind /sys /mnt/sys
mount --bind /run /mnt/run

Now, we'll enter the chroot environment. Notice how the terminal prompt changes after you run this.

chroot /mnt

Once inside, we need to set a couple of environment variables and run some workarounds. These commands are necessary to trick the package manager (apt) into working correctly in this minimal, non-booted environment. Without them, certain package installation scripts would fail.

# Set environment variables and run chroot workarounds
export HOME=/root
export LC_ALL=C
dbus-uuidgen > /etc/machine-id
ln -fs /etc/machine-id /var/lib/dbus/machine-id
dpkg-divert --local --rename --add /sbin/initctl
ln -s /bin/true /sbin/initctl

4b. Setting Up System Files and Software Sources

With the chroot environment ready, the first real configuration step is to set up the core system files.

First, we'll configure apt to know where to download software from. I'm using the German mirror in the new .sources format. You should change the de.archive.ubuntu.com URL to one that is closer to you for better speeds.

cat <<EOF > /etc/apt/sources.list.d/ubuntu.sources
Types: deb
URIs: http://de.archive.ubuntu.com/ubuntu/
Suites: noble noble-updates noble-backports
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg

Types: deb
URIs: http://security.ubuntu.com/ubuntu/
Suites: noble-security
Components: main restricted universe multiverse
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
EOF

Next, we'll create the /etc/fstab file. This file is critical, it tells the system which partitions to mount at boot time. The blkid command is used here to get the unique UUID for each partition, which is more reliable than using device names. The double dash is in ubuntu--vg is due to naming conventions.

cat <<EOF > /etc/fstab
/dev/mapper/ubuntu--vg-root / ext4 errors=remount-ro 0 1
UUID=$(blkid -s UUID -o value ${UBUNTU_DRIVE}p2) /boot ext4 defaults 0 2
UUID=$(blkid -s UUID -o value ${UBUNTU_DRIVE}p1) /boot/efi vfat umask=0077 0 2
/dev/mapper/ubuntu--vg-swap none swap sw 0 0
EOF

Finally, we'll create the /etc/crypttab file. This tells the system how to find and unlock our encrypted LUKS partition during the boot process.

echo "crypt UUID=$(blkid -s UUID -o value ${UBUNTU_DRIVE}p3) none luks,discard" > /etc/crypttab

The foundational files are now in place. We can now proceed with installing the rest of the software.

4c. Setting Up the Locale and System Identity

With the core files in place, we'll set up the system's language, regional formats, and name. This is done before the main software installation to prevent any package configuration errors.

First, we'll install the locales package. For my setup, I then generate the data for both English (for the user interface) and German (for formats like time, date, and currency).

apt install -y locales
locale-gen en_US.UTF-8
locale-gen de_DE.UTF-8

Next, we'll write the configuration to /etc/default/locale. This tells the system to use en_US.UTF-8 as the main language but de_DE.UTF-8 for all the formats.

cat <<EOF > /etc/default/locale
LANG="en_US.UTF-8"
LC_NUMERIC="de_DE.UTF-8"
LC_TIME="de_DE.UTF-8"
LC_MONETARY="de_DE.UTF-8"
LC_PAPER="de_DE.UTF-8"
LC_NAME="de_DE.UTF-8"
LC_ADDRESS="de_DE.UTF-8"
LC_TELEPHONE="de_DE.UTF-8"
LC_MEASUREMENT="de_DE.UTF-8"
LC_IDENTIFICATION="de_DE.UTF-8"
EOF

Now we'll set the system's hostname. You should change system-name to whatever you prefer. After that, we'll run apt update and apt upgrade to make sure all base packages are up to date before installing the desktop.

echo "system-name" > /etc/hostname
apt update
apt -y upgrade

4d. Installing the Kernel, Desktop, and Other Software

The following command will download and install the Linux kernel, the full Ubuntu Desktop environment, networking tools, drivers, and other essential utilities. This is the "Recommended Practical" package list that includes everything needed for a complete user experience. This step will take a while.

apt install -y cryptsetup lvm2 linux-generic grub-efi-amd64-signed shim-signed network-manager ubuntu-standard initramfs-tools ubuntu-desktop os-prober zstd nano ubuntu-restricted-addons ubuntu-restricted-extras

If you have secure boot disabled, change grub-efi-amd64-signed in the above command to grub-efi-amd64 and omit installing shim-signed.

4e. Creating a User Account

With all the software installed, we need to create our user account. The adduser command is interactive and will prompt you to set a password.

adduser your-name

After the user is created, we'll add them to the sudo group to grant administrative privileges. We'll also configure the system's timezone here.

usermod -aG sudo your-name
dpkg-reconfigure tzdata

4f. Making the System Bootable

The system is almost complete, but it doesn't know how to boot yet. This final phase inside the chroot will configure the GRUB bootloader.

First, we'll edit the GRUB configuration file to explicitly enable support for encrypted volumes and to ensure it runs os-prober to find our Windows installation.

echo "GRUB_ENABLE_CRYPTODISK=y" >> /etc/default/grub
echo "GRUB_DISABLE_OS_PROBER=false" >> /etc/default/grub

Now for a critical step. For os-prober to find the Windows bootloader, it needs to be able to see the Windows EFI partition. We'll create a temporary mount point and mount the Windows EFI partition there. Remember to use the ${WINDOWS_DRIVE} variable we set at the very beginning.

mkdir -p /tmp/win_efi
mount ${WINDOWS_DRIVE}p1 /tmp/win_efi

Finally, we'll run the three commands that build the boot files and install the bootloader.

  1. update-initramfs: Builds the initial RAM disk, making sure to include the drivers for LUKS and LVM so the system can unlock itself.
  2. grub-install: Installs the GRUB bootloader files to the EFI partition. If you do not intend to use secure boot omit the --uefi-secure-boot option.
  3. update-grub: Scans for all kernels and operating systems (including Windows, which it can now see) and generates the final grub.cfg menu file.
update-initramfs -u -k all
grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=GRUB --uefi-secure-boot
update-grub

Verification: Look at the output of the update-grub command. You must see a line that says something like "Found Windows Boot Manager on /dev/nvme0n1p1". If you don't see this, something is wrong.

Step 5: Cleanup and Final Reboot

The configuration is complete. Now we need to perform a clean exit, undoing all the temporary mounts and workarounds we put in place. This is a two-stage process: first cleaning up inside the chroot, then exiting and cleaning up the live session mounts.

5a. Cleaning Up Inside the chroot

We'll start by undoing the chroot workarounds, cleaning the apt cache to save a little space, and unmounting the Windows EFI partition we mounted earlier.

# Undo the chroot workarounds
truncate -s 0 /etc/machine-id
rm /sbin/initctl
dpkg-divert --rename --remove /sbin/initctl

# Unmount the Windows EFI partition
umount /tmp/win_efi

# Clean up apt cache and temporary files
apt clean
rm -rf /tmp/* ~/.bash_history

Now, we'll exit the chroot environment to return to the live USB's main terminal session.

exit

5b. Final Unmount and Reboot

Back in the live session, we need to unmount all the filesystems in the reverse order we mounted them. This includes the bind mounts for the kernel pseudo-filesystems, deactivating the LVM, and finally closing the LUKS encrypted container.

# Unmount the bind mounts
umount /mnt/dev
umount /mnt/proc
umount /mnt/sys
umount /mnt/run

# Deactivate LVM and close the LUKS container
vgchange -an
cryptsetup close crypt

Sometimes, the desktop environment can be stubborn and keep a handle on the LUKS device, causing the cryptsetup close command to fail with a "device busy" error. If that happens, don't worry. The vgchange -an command is the most important one, and a reboot will cleanly close everything else.

Now for the final command.

reboot

When the screen goes black, remove the live USB drive.