Join Nostr
2026-07-19 13:02:32 UTC
in reply to

linux_privacy on Nostr: Encrypted AppVMs and Templates. Encrypted pool. Secret vault in a LUKS-Pool. :gear: ...

Encrypted AppVMs and Templates. Encrypted pool. Secret vault in a LUKS-Pool.

:gear: Install:

Make sure there is sufficient free space (more than 3 GB) in dom0. You can resize dom0 these commands:

sudo lvresize --size 3G /dev/mapper/qubes_dom0-root
sudo resize2fs /dev/mapper/qubes_dom0-root
sudo lvresize -L +3G 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-5 which 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 3 GB encrypted pool vault
(edit POOL_SIZE="3G" 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="3G"
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 "[*] 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

# 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 YES to 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 qube and in Advanced select vault in Storage pool. Or create a new qube, and select vault Storage pool in the Advanced Options.
1
1751×251 21.3 KB

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.

:eyes: :eyes:

Opening large pools takes time. If you created a large pool and it contains significant data (tens of GB), opening it with vault-open may 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.

:bomb: 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 "========================================"