Dual-booting Windows next to an encrypted Ubuntu

Shrinking a LUKS + LVM Ubuntu to make room for Windows, then getting GRUB back. The exact steps, and the failures most guides skip.

← Writing

My laptop had Ubuntu with full-disk encryption on the whole drive, and I needed Windows next to it. Most dual-boot guides assume a plain disk and stop working once encryption is involved. This is what worked for me, with Ubuntu installed first and Windows added second.

Every command block says where it runs. Some run on your installed Ubuntu, some on the live USB, and running one in the wrong place can break things. The outputs are from my machine, trimmed a little. Your sizes and IDs will be different.

Installed Ubuntu · Root shell

sudo rsync -aAXH --info=progress2 / /mnt/backup \
  --exclude={"/dev/*","/proc/*","/sys/*","/run/*","/tmp/*","/mnt/*","/media/*","/lost+found"}

sudo cryptsetup luksHeaderBackup /dev/nvme0n1p3 \
  --header-backup-file /mnt/backup/luks-header.img

output

  6,318,442,496  41%    5.40MB/s    0:18:22 (xfr#412183, to-chk=18002/2600035)

If rsync ends with exit code 23, some files didn’t copy. Scroll up and check which ones. On my machine they were all virtual files under /var/lib/lxcfs, which you don’t need, but don’t assume that until you’ve looked.

Why it takes two steps

An encrypted Ubuntu install is three layers inside each other. The disk partition holds a LUKS container (the encryption). Inside that is LVM, which splits the space into volumes. Inside LVM is the ext4 filesystem with your files.

You shrink from the inside out. First the filesystem and its volume, then the partition around them:

After lvreduce inner layers shrunk
LUKS partition · 476 GiB · unchanged
LVM · vgubuntu · 412 GiB used
ext4 · root
411 GiB · your files
64 GiB free inside,
partition still full size
→
After GParted partition shrunk to match
LUKS partition · 412 GiB
LVM · vgubuntu · 412 GiB
ext4 · root
411 GiB · your files
64 GiB unallocated
on disk → for Windows
1lvreduce shrinks the filesystem and its volume. Root drops from 475 to 411 GiB. The partition around it stays the same size, so the 64 GiB is free but still inside it (left). 2GParted shrinks the partition to match. It resizes LVM and the LUKS container along with the partition, and the 64 GiB becomes unallocated space on the disk (right). !You need both steps. After only step 1, the free space is stuck inside the partition and the Windows installer can't see it.

Find your device names

On your installed system, check what your disk and partitions are called:

Installed Ubuntu · Terminal

lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINT

output

NAME                    SIZE FSTYPE      MOUNTPOINT
nvme0n1               476.9G
├─nvme0n1p1             512M vfat        /boot/efi      ← EFI
├─nvme0n1p2             1.4G ext4        /boot          ← boot
└─nvme0n1p3             476G crypto_LUKS                ← LUKS
  └─nvme0n1p3_crypt     476G LVM2_member                ← LVM
    ├─vgubuntu-root     475G ext4        /              ← your files
    └─vgubuntu-swap_1   976M swap        [SWAP]

NVMe disks are called nvme0n1, SATA disks sda. Every command below uses the names from my machine, so swap in yours. A wrong name means a wrong disk.

Mine are:

  • LUKS partition: nvme0n1p3
  • /boot: nvme0n1p2
  • EFI: nvme0n1p1
  • whole disk: nvme0n1

Note the name of the unlocked device too (nvme0n1p3_crypt here). You’ll reuse it from the live USB so the paths match.

Shrink Ubuntu

Boot the Ubuntu live USB and pick Try Ubuntu. Everything in this section runs there, because you can’t shrink a filesystem while it’s mounted and in use.

Unlock the partition. It asks for your passphrase:

Live USB · Terminal

sudo cryptsetup luksOpen /dev/nvme0n1p3 nvme0n1p3_crypt

It prints nothing if it worked. A wrong passphrase gives No key available with this passphrase.

Check the LVM volumes:

Live USB · Terminal

sudo lvs

output

  LV     VG       Attr       LSize    Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert
  root   vgubuntu -wi-a----- <475.00g
  swap_1 vgubuntu -wi-a-----  976.00m

If this shows nothing, run sudo vgchange -ay and try again.

Now shrink root by 64 GiB. The -r makes LVM resize the ext4 filesystem along with the volume. Without it you’d cut the volume under a filesystem that still thinks it’s full size, and you’d lose data.

64 GiB is just over Windows 11’s 64 GB minimum. Take more if you’ll install anything on Windows.

Live USB · Terminal

sudo lvreduce -r -L -64G /dev/vgubuntu/root

output

  Found volume group "vgubuntu"
  fsck from util-linux 2.37.2
  /dev/mapper/vgubuntu-root: clean, 1.2M/30M files, 80G/124G blocks
  resize2fs 1.46.5 (30-Dec-2021)
  Resizing the filesystem on /dev/mapper/vgubuntu-root to 107515904 (4k) blocks.
  The filesystem on /dev/mapper/vgubuntu-root is now 107515904 (4k) blocks long.
  Size of logical volume vgubuntu/root changed from <475.00 GiB to <411.00 GiB.
  Logical volume vgubuntu/root successfully resized.

Then the partition, in GParted:

1Select the crypt-luks partition (/dev/nvme0n1p3). 2Click Resize/Move and enter your passphrase if it asks. 3Make it smaller by the 64 GiB you freed, and no more. 4Click Apply.

With the resize queued, it looks like this:

/dev/nvme0n1 - GParted 476.94 GiB
GPartedEditView DevicePartitionHelp
NewDeleteResize/MoveCopyPasteApply /dev/nvme0n1 (476.94 GiB) ▾
/dev/nvme0n1p3crypt-luks · 411.97 GiB
unallocated64.00 GiB
PartitionFile SystemSizeUsed
🔒 /dev/nvme0n1p3 crypt-luks411.97 GiB~80 GiB
unallocated unallocated64.00 GiB—
1 operation pending: Shrink /dev/nvme0n1p3 from 475.97 GiB to 411.97 GiB
The freed 64 GiB shows up as grey unallocated space at the end of the disk. That's where Windows goes.

If GParted won’t shrink it

Mine failed the first time with “cannot resize, later extents are allocated”. Something was still sitting at the end of the partition. Look at where each volume is:

Live USB · Terminal

sudo pvs --segments -o pv_name,lv_name,seg_start_pe,seg_size_pe

output

  PV                          LV      Start  SSize
  /dev/mapper/nvme0n1p3_crypt root        0 105215
  /dev/mapper/nvme0n1p3_crypt             105215  16384
  /dev/mapper/nvme0n1p3_crypt swap_1 121599    250

root is at the front. The middle row has no LV name, so that’s the free space lvreduce made. Swap comes after it, at the very end, and that’s what blocks the shrink. GParted can only cut free space off the end.

The fix is to move swap into the gap right after root. Swap starts at extent 121599 and is 250 extents long, so it covers 121599-121848 (the end is start + size − 1). The gap starts at 105215, so the target is 105215-105464:

Live USB · Terminal

sudo pvmove --alloc anywhere \
  /dev/mapper/nvme0n1p3_crypt:121599-121848 \
  /dev/mapper/nvme0n1p3_crypt:105215-105464

output

  /dev/mapper/nvme0n1p3_crypt: Moved: 5.08%
  /dev/mapper/nvme0n1p3_crypt: Moved: 41.27%
  /dev/mapper/nvme0n1p3_crypt: Moved: 100.00%

Use your own numbers from pvs --segments. Wait for 100.00%, then run pvs --segments again and check that the free row is now last. After that the GParted resize goes through.

Make the Windows USB

Rufus is the usual tool for this, but it only runs on Windows. On Linux most people use WoeUSB, and it has a catch.

Windows 11’s install.wim is bigger than 4 GB (7.06 GB in my ISO), and FAT32, the filesystem UEFI can boot from, can’t hold a file over 4 GB. So WoeUSB puts the files on an NTFS partition and adds a small bootloader, UEFI:NTFS, which loads an NTFS driver at boot. On some machines that driver fails to start:

UEFI:NTFS: Unable to start driver: [2] Invalid Parameter

That’s what happened on mine. You can get around it with FAT32 by splitting install.wim into smaller files, but I found Ventoy easier. You install it on the stick once, then just copy ISOs onto it:

Installed Ubuntu · Terminal

# erases the stick, check it's really /dev/sda with lsblk first
sudo sh Ventoy2Disk.sh -i /dev/sda

sudo mkdir -p /mnt/ventoy
sudo mount /dev/sda1 /mnt/ventoy
sudo cp ~/Downloads/Win11.iso /mnt/ventoy/
sync
sudo umount /mnt/ventoy

output of the Ventoy install

Disk : /dev/sda
Model: SanDisk Cruzer Blade (scsi)
Size : 14 GB

Continue? (y/n) y
Double-check. Continue? (y/n) y

Create partitions on /dev/sda by parted in MBR style ...
Done
Install Ventoy to /dev/sda successfully finished.

The cp shows no progress and it’s slow. An 8.5 GB ISO took about 25 minutes on my cheap stick. Wait for umount to finish before you pull it out.

Boot the stick in UEFI mode. If your boot menu lists it twice, pick the entry that starts with UEFI:. Booted the other way, Windows setup won’t install onto a GPT disk like this one.

Install Windows

1Boot the Ventoy stick and pick the Windows ISO from its menu. 2Run setup and choose Custom install. 3Select the unallocated space you freed, not any existing partition. 4Let it install and reboot into Windows.

To skip the Microsoft account, I pressed Shift+F10 on the network screen and ran:

Windows Setup · Shift+F10 prompt

OOBE\BYPASSNRO

Setup restarted with an I don’t have internet option, and Continue with limited setup gave me a local account. Microsoft has removed this from newer Windows 11 builds, so it may not work with your ISO.

Get back into Ubuntu

After the install, the laptop boots straight into Windows. Ubuntu is still there. Windows just put its own entry first in the UEFI boot order.

So try the simple fix first. Open your firmware’s boot menu (F12 on many laptops), pick ubuntu, and once you’re in, move it back to the top:

Installed Ubuntu · Terminal

sudo efibootmgr                  # lists entries and the current BootOrder
sudo efibootmgr -o 0001,0000     # your ubuntu entry's number first, then Windows

If there’s no ubuntu entry at all, reinstall GRUB from the live USB. The live system can’t see your installed Ubuntu until you unlock and mount it, so do that first:

Live USB · Terminal

sudo cryptsetup luksOpen /dev/nvme0n1p3 nvme0n1p3_crypt
sudo mount /dev/mapper/vgubuntu-root /mnt
sudo mount /dev/nvme0n1p2 /mnt/boot
sudo mount /dev/nvme0n1p1 /mnt/boot/efi
findmnt -R /mnt

output

TARGET            SOURCE                     FSTYPE OPTIONS
/mnt              /dev/mapper/vgubuntu-root  ext4   rw,relatime
└─/mnt/boot       /dev/nvme0n1p2             ext4   rw,relatime
  └─/mnt/boot/efi /dev/nvme0n1p1             vfat   rw,relatime

Then switch into it and install GRUB to the EFI partition. GRUB’s own tools have to run from inside your installed system, which is why you need chroot and the extra mounts:

Live USB · Terminal

for d in dev proc sys run; do sudo mount --rbind /$d /mnt/$d; done
sudo chroot /mnt grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=ubuntu
sudo chroot /mnt update-grub

output

Installing for x86_64-efi platform.
Installation finished. No error reported.

Reboot, and GRUB is back.

Add Windows to the GRUB menu

GRUB only lists Ubuntu at first. It finds other systems with a tool called os-prober, and Ubuntu ships with that turned off. On your installed system:

Installed Ubuntu · Terminal

sudo nano /etc/default/grub

Find this line near the bottom and remove the #:

in the editor

- #GRUB_DISABLE_OS_PROBER=false
+ GRUB_DISABLE_OS_PROBER=false

If the line isn’t there, add it. Save with Ctrl+O, Enter, then Ctrl+X, and rebuild the menu:

Installed Ubuntu · Terminal

sudo update-grub

output

Sourcing file `/etc/default/grub'
Generating grub configuration file ...
Found linux image: /boot/vmlinuz-6.8.0-124-generic
Found initrd image: /boot/initrd.img-6.8.0-124-generic
Found Windows Boot Manager on /dev/nvme0n1p1@/EFI/Microsoft/Boot/bootmgfw.efi
done

You want the Found Windows Boot Manager line. If it’s missing, check that the change saved and that the os-prober package is installed (sudo apt install os-prober).

Reboot, and the menu has both systems. If a Windows update ever puts its entry back on top, fix it the same way, with the firmware boot menu and efibootmgr.