Accessing LUKS and ext4 drives from macOS

January 13, 2026


Blog post social card

Recently I needed to access my LUKS-encrypted, ext4 drives from a MacBook. This can come in handy in a few situations — future-proofing backups, needing to access Linux drives in a pinch while the NAS is down, etc — so here’s a quick playbook for my future self and others in the same position!

Overview

Unfortunately ext4 and LUKS are not supported natively by macOS so accessing the data from a Mac requires extra tooling. I’ve aimed for a lightweight and minimal, mostly stateless solution; this in turn should make it reliable and future-proof (I’ll report back in ten years from now). To minimize the risk of bit-rot, the only dependency beyond stock macOS is (the amazing) QEMU.

In the next section I’ll outline the solution, then walk through the actual playbook I use when accessing those pesky Linux drives from a Mac. In the final section we’ll wrap up by looking at some issues with the playbook and possible improvements. Let’s go!

Architecture

The solution works by spinning up a virtual machine (VM) running a minimal Ubuntu image. The VM is created using QEMU, the lightweight CLI tool that also powers Docker Desktop and Podman Desktop on macOS. Because the external drive is passed raw to the VM and because QEMU supports Apple’s Hypervisor Framework for hardware acceleration, the VM runs smoothly and the drive’s data can be accessed efficiently.

The Ubuntu system is set up using cloud-init, which is a collection of tools and interfaces more often used by cloud services like AWS to set up new machines and instances automatically, which makes this setup — and in particular the SSH config — automated and reproducible. SSH is used both for accessing the Ubuntu system (to e.g. decrypt the LUKS partition) and to transfer the files between the VM and macOS.

image

To recap, the external drive is connected to the MacBook but the content is not directly accessed from macOS; instead the drive is passed through to an Ubuntu VM created with QEMU. The Ubuntu system is configured with cloud-init to allow SSH access from the macOS host, for terminal access (for LUKS and mount operations) and then also for transferring files. Note that macOS never mounts or interprets the encrypted filesystem; all LUKS and ext4 handling happens inside the VM.

Let’s now see how the VM is set up and how the files can be transferred!

Playbook

We need to prepare a few images required by the VM to boot. We’ll be downloading and creating a few files, so it’s best to move to an empty directory:

cd $(mktemp -d)

Download the official Ubuntu image:

curl -LO https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-arm64.img
Note

You can also pin an old (specific) image by date. In my experience however using the latest (current) image is the most reliable: the tools we use from Ubuntu (SSH, LUKS) don’t (or very rarely) change interface, but old Ubuntu images are not stored indefinitely by Canonical.

Create the cloud-init data:

mkdir -p ./cloud-init
cat << EOF > ./cloud-init/user-data
#cloud-config
chpasswd:
  expire: False
ssh_pwauth: False
ssh_authorized_keys:
$(cat ~/.ssh/*.pub | sed 's/^/  - /')
EOF

cat << EOF > ./cloud-init/meta-data
instance-id: my-network/my-vm
EOF

All cloud-init configuration is done with YAML. The first file, ./cloud-init/user-data, sets up a single user and disables password authentication, only allowing SSH access. The public key(s) are read from the host (i.e. Macbook Air). The value in ./cloud-init/meta-data is required for cloud-init to run successfully though the specific content does not matter much (see the cloudinit reference for more information).

For Ubuntu to correctly recognize the cloud-init data, it needs to be mounted as a CD-ROM with volume name cidata.

Create an ISO image with volume name cidata with content from ./cloud-init, using the macOS tool hdiutil:

hdiutil makehybrid -iso -joliet -default-volume-name cidata -o ./seed.iso ./cloud-init

Now we need QEMU. I use Nix to install QEMU from nixpkgs, as it allows an ephemeral install without polluting the system.

Install QEMU from nixpkgs:

nix build nixpkgs#qemu
PATH="$PWD/result/bin:$PATH"
QEMU="$PWD/result/share/qemu"
qemu-system-aarch64 --version
Note

QEMU can also be installed with brew, see https://www.qemu.org/download/#macos.

Ubuntu needs some firmware to boot under UEFI on Apple Silicon. Said firmware should be shipped as part of the QEMU install.

Locate the aarch64 firmware in the QEMU install:

ls "$QEMU/edk2-aarch64-code.fd" # this file should exist

The boot process also requires an additional, blank 64MB drive. Use the qemu-img tool from QEMU to create a sparse empty image:

qemu-img create -f qcow2 varstore.img 64M

At this point we have the OS image ready, the cloud-init “CD-ROM” image, as well as two boot-related images. All these will be connected to the VM; now let’s actually set up the physical (external drive).

Connect the external drive to the MacBook and when prompted tell macOS to ignore it:

image

Warning

From here on, do not plug or unplug any disks. Whenever a new disk is plugged in, macOS rescans all the disks and might attempt to mount /dev/disk4 again which will wreak havoc in the VM.

We need to identify the disk number macOS assigned to the external drive.

Use diskutil list and look for a drive labeled as external, physical, which has a partition of type Linux and a SIZE that matches the drive’s:

$ diskutil list
/dev/disk0 (internal, physical):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:      GUID_partition_scheme                        *251.0 GB   disk0
   ...

/dev/disk3 (synthesized):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:      APFS Container Scheme -                      +245.1 GB   disk3
                                 Physical Store disk0s2
   1:                APFS Volume Macintosh HD            12.2 GB    disk3s1
   ...

/dev/disk4 (external, physical): 
   #:                       TYPE NAME                    SIZE       IDENTIFIER 
   0:     FDisk_partition_scheme                        *2.0 TB     disk4
   1:                      Linux                         2.0 TB     disk4s1

...
Warning

In the next two commands replace disk4 with your disk. Passing in the wrong disk might incur unexpected data loss, I’m not responsible for you erasing your data in any way, etc, etc. Enjoy responsibly.

Unmount the drive:

diskutil unmountDisk /dev/disk4

Starting the VM

Start the Ubuntu VM with the external drive:

Note

Note the use of /dev/rdisk4 (instead of /dev/disk4) so that the disk is passed through “raw”. This gives Ubuntu direct control over the disk, without QEMU or macOS interfering.

sudo qemu-system-aarch64 \
  -cpu host -M virt,accel=hvf -m 2G \
  -drive file="$QEMU/edk2-aarch64-code.fd",if=pflash,format=raw,readonly=on \
  -drive file=varstore.img,if=pflash \
  -drive file=./noble-server-cloudimg-arm64.img,format=qcow2,if=virtio \
  -drive file=seed.iso,index=1,media=cdrom \
  -drive file=/dev/rdisk4,if=virtio,format=raw,cache=none \
  -nographic -nic user,hostfwd=tcp:127.0.0.1:2222-:22

This creates a VM with 2 GB of RAM (-m 2G) and specifies the CPU as host using Apple’s Hypervisor Framework (hvf) to avoid software CPU emulation.

Five -drives are attached to the machine:

We do also map the VM’s SSH port (22) to a free port on the macOS host (here 2222) so that we can connect to the VM via SSH.

After a few seconds the VM is up and running and accessible via SSH. For more information on QEMU’s CLI options refer to the official documentation.

Decrypting and accessing the drive

SSH into the VM:

# from macOS
SSH_CMD='ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p 2222'
eval "$SSH_CMD ubuntu@localhost"

It’s handy to keep the SSH command in a variable so it can be reused with other commands like rsync. Host key checking is disabled because I spin up a new VM every time.

Now identify the relevant partition inside the VM:

ubuntu@ubuntu:~$ sudo lsblk
NAME    MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTS
vda     253:0    0  3.5G  0 disk
├─vda1  253:1    0  2.5G  0 part /
├─vda15 253:15   0   99M  0 part /boot/efi
└─vda16 259:0    0  923M  0 part /boot
vdb     253:16   0  900K  1 disk
vdc     253:32   0  1.8T  0 disk
└─vdc1  253:33   0  1.8T  0 part

Decrypt and mount the partition:

# from Linux VM via SSH
sudo cryptsetup luksOpen /dev/vdc1 ext-drive # will ask for device passphrase
sudo mkdir -p /mnt/ext-drive
sudo mount /dev/mapper/ext-drive /mnt/ext-drive

The decrypted drive content is accessible!

Transfer content between the VM and macOS using rsync:

# from macOS
rsync --archive --verbose --human-readable --progress \
    --rsh "$SSH_CMD" \
    ./my-new-backup/ ubuntu@localhost:/mnt/ext-drive/my-backup-v42

The first argument turns on archive mode so that files are copied recursively. We also enable verbose, human readable output, as well as progress reporting. The --rsh argument specifies a custom SSH command to use; if you followed along then $SSH_CMD should contain the ssh command with port. The last two arguments specify what to copy where; in general rsync src/ dest will copy every file from src/ into dest, such that src/foo becomes dest/foo (same as cp)

Once we’re done copying or playing with the files, we can umount the drive, close it from a LUKS point of view, and power off the VM:

# from Linux VM via SSH
sudo umount /mnt/ext-drive
sudo cryptsetup close ext-drive
sudo poweroff

Finally, we can turn off the drive from macOS:

# from macOS
diskutil eject /dev/disk4

Wrapping up

That’s it for the playbook!

While this works pretty well in practice, there are a few things that could be improved. For instance, it would be nice to have somewhat more native integration with Finder — this could be done with sshfs (somewhat deprecated as I understand) or by setting up Samba from within Ubuntu and connecting to it from Finder.

There’s also the issue that, as it is now, it’s a bunch of commands you have to get right; get one wrong and you might wipe your system or backups. I like running them manually so that I can double check everything before hitting enter, but over time I might start porting them to a small script, or even better a full-blown app (I’ve been looking for an excuse to try Tauri for a while).

For now, I’ll stick to the playbook. Feel free to use it and tweak it — at your own risk of course!

Let me know if you enjoyed this article! You can also subscribe to receive updates.


Here's more on the topic of LUKS and Linux: