In this guide, we will create an encrypted storage pool for sensitive VMs - such as a vault - that is protected by its own separate LUKS passphrase. This is not your system disk password entered at boot; it is an independent passphrase used exclusively to unlock the vault pool when you choose to open it. Secret VMs are hidden when the pool is closed (hidden from app menu and qube manager).
$ qvm-pool
NAME DRIVER
varlibqubes file
linux-kernel linux-kernel
vm-pool lvm_thin
vault lvm_thin
This guide solves the old problems:
- Per-VM encryption
- Protecting a sensitive qube as a mitigation in case of physical attacks like device theft
Many users have been requesting this functionality for a long time. In my anti-forensics guide, I used an ephemeral encrypted dm-container in overlay mode, but we can build a persistent encrypted container protected by a passphrase, which will serve as the backing store for a dedicated secret pool.
How it works: the setup script performs the following steps. First, it allocates a large sparse file (vault.img) and attaches it as a loop device. Then it formats the loop device with LUKS encryption and opens it, creating a decrypted mapper device. On top of that mapper, we build an LVM thin pool (PV → VG → thin LV). The thin pool is then registered with Qubes as a custom lvm_thin pool named vault. Two helper scripts are installed in /usr/local/bin/:
vault-open- attaches the loop device, unlocks the LUKS container with your passphrase, activates the volume group, registers the pool in Qubes, adds hidden VMs to the app menu and qube manager, so your secret VMs can boot.vault-close- gracefully shuts down any running VMs from the pool, deactivates LVM, closes the LUKS mapper,detaches the loop device, returning the pool to a fully encrypted offline state, hides secret VMs from the app menu and qube manager.
This gives you a seamless workflow: run sudo vault-open, start your sensitive VMs, and when finished, run sudo vault-close to lock everything away.
When the pool is closed, cryptsetup close removes the existing mapping and wipes the encryption key from kernel memory - this is explicitly documented in the cryptsetup manual. Once the key is erased from RAM, the encrypted container becomes inaccessible, and even a physical memory dump or cold boot attack cannot recover the volume key. The kernel securely erases the passphrase and any derived keys from memory, making this approach resistant to forensic memory analysis. This is a significant improvement over simply unmounting a filesystem, where encryption keys might linger in RAM.
Install:
- Make sure there is sufficient free space (more than 5 GB) in dom0. You can resize dom0 these commands (edit
5Gif need mode vault space):
sudo lvresize --size 5G /dev/mapper/qubes_dom0-root
sudo resize2fs /dev/mapper/qubes_dom0-root
sudo lvresize -L +5G qubes_dom0/root-pool
- I recommend disabling disk swap for maximum security: Qubes OS uses zram for compressed RAM swap, but also maintains a fallback swap partition at
/dev/dm-5which is not encrypted. For a vault pool setup, this is a security risk - sensitive memory pages from your secret VMs could be written to disk in plaintext.
sudo sed -i '/\/dev\/mapper\/swap/!{/^[[:space:]]*#/!{/\<swap\>/s/^/# /}}' /etc/fstab
sudo sed -i '/\/dev\/mapper\/swap/b; /[[:space:]]\+swap[[:space:]]\+/s/^/# /' /etc/fstab
sudo dracut --force
- Run this simple script for create 5 GB encrypted pool vault
(editPOOL_SIZE="5G"if you need a different pool size)
#!/bin/bash
set -euo pipefail
POOL_DIR="/var/lib/qubes/pools"
POOL_IMG="$POOL_DIR/vault.img"
POOL_SIZE="5G"
VG_NAME="vault_vg"
LV_NAME="vault_thin"
MAPPER_NAME="vault_crypt"
cleanup() {
local dev="${LOOP_DEV:-}"
if [ -n "$dev" ] && losetup -a | grep -q "$dev"; then
echo "[*] Cleanup: detaching $dev"
losetup -d "$dev" 2>/dev/null || true
fi
}
trap cleanup EXIT
echo "========================================"
echo " Creating an encrypted Qubes pool"
echo "========================================"
echo
if [ "$EUID" -ne 0 ]; then
echo "[!] This script must be run as root (dom0)"
exit 1
fi
if [ -f "$POOL_IMG" ]; then
echo "[!] File $POOL_IMG already exists."
read -r -p " Delete and recreate? Type YES to confirm: " confirm
if [ "$confirm" != "YES" ]; then
echo "[!] Aborted by user"
exit 1
fi
rm -f "$POOL_IMG"
fi
echo "[*] Creating directory $POOL_DIR"
mkdir -p "$POOL_DIR"
echo "[*] Creating a loop file of size $POOL_SIZE"
truncate -s "$POOL_SIZE" "$POOL_IMG"
echo "[*] Attaching loop device"
LOOP_DEV=$(losetup -f --show "$POOL_IMG")
echo " Device: $LOOP_DEV"
echo
echo "[*] Encrypting device $LOOP_DEV"
echo " Enter the LUKS passphrase (twice)"
cryptsetup luksFormat "$LOOP_DEV"
echo
echo "[*] Opening the LUKS container"
cryptsetup open "$LOOP_DEV" "$MAPPER_NAME"
echo
echo "[*] Creating LVM: PV -> VG -> Thin Pool"
pvcreate "/dev/mapper/$MAPPER_NAME"
vgcreate "$VG_NAME" "/dev/mapper/$MAPPER_NAME"
lvcreate -T -n "$LV_NAME" -l +100%FREE "$VG_NAME"
echo
echo "[*] Detaching the loop device (LVM stays active)"
losetup -d "$LOOP_DEV"
unset LOOP_DEV
qvm-pool --add vault lvm_thin \
-o volume_group=vault_vg,thin_pool=vault_thin,revisions_to_keep=2
echo
echo "========================================"
echo " Pool created successfully!"
echo "========================================"
cat > /usr/local/bin/vault-open << 'EOF'
#!/bin/bash
# open-and-register-vault.sh
set -euo pipefail
if [ "$EUID" -ne 0 ]; then
echo "[!] Must be run as root"
exit 1
fi
# 1. Attach loop
LOOP_DEV=$(losetup -f --show /var/lib/qubes/pools/vault.img)
echo "[*] Loop: $LOOP_DEV"
# 2. Unlock LUKS
cryptsetup open "$LOOP_DEV" vault_crypt
echo "[*] LUKS opened"
# 3. Activate LVM
vgchange -ay vault_vg
echo "[*] LVM activated"
# 4. Register pool in Qubes if not already present
if ! qvm-pool --list | grep -q "^vault "; then
echo "[*] Registering pool in Qubes"
qvm-pool --add vault lvm_thin \
-o volume_group=vault_vg,thin_pool=vault_thin,revisions_to_keep=2
else
echo "[*] Pool already registered"
fi
echo "[*] Restoring visibility of VMs..."
for vm in $(qvm-ls --raw-list 2>/dev/null); do
[ -n "$vm" ] || continue
if qvm-volume list "$vm" 2>/dev/null | grep -q "${POOL_NAME}"; then
current_internal=$(qvm-features "$vm" internal 2>/dev/null || echo "")
if [ "$current_internal" = "1" ]; then
echo " -> Unhiding: $vm"
qvm-features "$vm" internal '' 2>/dev/null || true
fi
fi
done
echo "[*] Done. Verification:"
qvm-pool --info vault
EOF
cat > /usr/local/bin/vault-close << 'EOF'
#!/bin/bash
# close-vault.sh
set -euo pipefail
if [ "$EUID" -ne 0 ]; then
echo "[!] Must be run as root"
exit 1
fi
# 1. Stop all running VMs from the vault pool
RUNNING_VMS=$(qvm-ls --running --fields=name,pool | grep vault | awk '{print $1}' || true)
if [ -n "$RUNNING_VMS" ]; then
echo "[*] Stopping VMs from the vault pool..."
for vm in $RUNNING_VMS; do
echo " -> $vm"
qvm-shutdown --wait "$vm"
done
echo "[*] All vault pool VMs stopped"
fi
# Hide secret VMs
for vm in $(qvm-ls --raw-list 2>/dev/null); do
[ -n "$vm" ] || continue
if qvm-volume list "$vm" 2>/dev/null | grep -q "${POOL_NAME}"; then
echo " -> Hiding: $vm"
qvm-features "$vm" internal 1 2>/dev/null || true
fi
done
# 2. Deactivate LVM
vgchange -an vault_vg || true
# 3. Close LUKS (wipes key from kernel memory)
cryptsetup close vault_crypt || true
# 4. Detach loop
LOOP_DEV=$(losetup -j /var/lib/qubes/pools/vault.img 2>/dev/null | head -1 | cut -d: -f1)
if [ -n "$LOOP_DEV" ]; then
losetup -d "$LOOP_DEV"
fi
echo "[*] Pool locked and key wiped from memory"
EOF
-
Confirm creation: When prompted, type
YESto proceed (this overwrites any existing vault pool file). -
Set your LUKS passphrase: Enter and confirm a strong passphrase when prompted - this will be required every time you unlock the pool.
-
Open the pool when needed in dom0:
sudo vault-open
and enter your LUKS passphrase. -
Assign VMs to the vault pool via Qubes Manager: click
clone qubeand inAdvancedselect vault inStorage pool. Or create a new qube, and select vaultStorage poolin theAdvanced Options.
Now your secret VMs can run. -
Close the pool when finished:
sudo vault-close
this shuts down all vault VMs, deactivates LVM, wipes the encryption key from kernel memory, and locks the container. Secret VMs will disappear from the app menu and Qube Manager after the pool is closed.
![]()
-
Opening large pools takes time. If you created a large pool and it contains significant data (tens of GB), opening it with
vault-openmay take 10-20 seconds - this is normal, as LVM needs to scan and activate the thin pool metadata. -
Backup the container along with your VMs. You can copy the encrypted container file together with backups of your AppVMs from this pool. The container is located at:
/var/lib/qubes/pools/vault.img
Since the container is fully encrypted, you can store it on external media or in cloud storage without additional encryption - the LUKS passphrase protects all data inside. -
If you need additional disk swap, you can create an ephemeral encrypted swap - there’s a simple guide in the description of this guide.
Removing Encrypted Pool
To completely remove the vault pool and all associated data, use this script. This will permanently delete all VMs stored in the pool.
(When prompted, type DELETE to proceed - this permanently destroys all vault VMs and their data)
#!/bin/bash
set -euo pipefail
POOL_NAME="vault"
POOL_DIR="/var/lib/qubes/pools"
POOL_IMG="$POOL_DIR/vault.img"
VG_NAME="vault_vg"
LV_NAME="vault_thin"
MAPPER_NAME="vault_crypt"
echo "========================================"
echo " Removing encrypted pool"
echo "========================================"
echo
if [ "$EUID" -ne 0 ]; then
echo "[!] This script must be run as root (dom0)"
exit 1
fi
echo "[!] WARNING: ALL data in the pool will be destroyed!"
read -r -p " Type DELETE to confirm: " confirm
if [ "$confirm" != "DELETE" ]; then
echo "[!] Aborted by user"
exit 1
fi
for vm in $(qvm-ls --raw-list 2>/dev/null); do
if qvm-volume list "$vm" 2>/dev/null | grep -q "${POOL_NAME}"; then
echo " -> Removing VM: $vm"
qvm-kill "$vm" 2>/dev/null || true
sleep 1
qvm-remove --force "$vm" 2>/dev/null || true
sleep 1
fi
done
if lvs "$VG_NAME/$LV_NAME" &>/dev/null; then
echo "[*] Deactivating thin pool $VG_NAME/$LV_NAME"
lvchange -an "$VG_NAME/$LV_NAME" || true
fi
if vgs "$VG_NAME" &>/dev/null; then
echo "[*] Deactivating Volume Group $VG_NAME"
vgchange -an "$VG_NAME" || true
fi
if dmsetup info "$MAPPER_NAME" &>/dev/null; then
echo "[*] Closing LUKS container /dev/mapper/$MAPPER_NAME"
cryptsetup close "$MAPPER_NAME" || true
else
echo "[*] LUKS container already closed"
fi
if vgs "$VG_NAME" &>/dev/null; then
echo "[*] Removing Volume Group $VG_NAME"
vgremove -y "$VG_NAME" || true
else
echo "[*] Volume Group $VG_NAME not found or already removed"
fi
if [ -f "$POOL_IMG" ]; then
LOOP_DEV=$(losetup -j "$POOL_IMG" 2>/dev/null | head -1 | cut -d: -f1)
if [ -n "$LOOP_DEV" ]; then
echo "[*] Detaching loop device $LOOP_DEV"
losetup -d "$LOOP_DEV" 2>/dev/null || true
else
echo "[*] Loop device already detached"
fi
fi
if [ -f "$POOL_IMG" ]; then
echo "[*] Deleting container file $POOL_IMG"
rm -f "$POOL_IMG"
fi
if [ -d "$POOL_DIR" ] && [ -z "$(ls -A "$POOL_DIR" 2>/dev/null)" ]; then
echo "[*] Removing empty directory $POOL_DIR"
rmdir "$POOL_DIR" 2>/dev/null || true
fi
qvm-pool remove $POOL_NAME
echo
echo "========================================"
echo " Pool removed"
echo "========================================"
License:Unlicense
Also see these my guides:
- Qubes OS live mode. dom0 in RAM. Non-persistent Boot. RAM-Wipe. Protection against forensics. Tails mode. Hardening dom0. Root read‑only. Paranoid Security. Ephemeral Encryption
- Alternative duress passwords for selective VMs destruction or system destruction in coercive environments. Paranoid security and privacy. Anti-forensics
- Installation of Amnezia VPN and Amnezia WG: effective tools against internet blocks via DPI for China, Russia, Belarus, Turkmenistan, Iran. VPN with Vless XRay reality. Best obfuscation for WireGuard. Easy self‑hosted VPN. Bypass
- Antidetect‑appVM with FOSS Antidetect Browsers. Windows fingerprint. Random fingerprint in dvm
