Ephemeral DVMs in fully ephemeral thin pools (ephemeral encryption, zram-disk, tmpfs). Removing DVM logs and metadata

All my scripts with License:Unlicense

:white_check_mark: This guide solves the old problems:

I’ve created a dedicated guide for maximum paranoid anti-forensic protection in Qubes OS, where dom0 also features strong hardening against hacking attacks. However, not all Qubes users want to boot dom0 into new modes and then reboot to apply dom0 updates. For users seeking forensic resistance without the complexity of running dom0 in amnesic-live mode, fully ephemeral Disposable VM (DVMs) offer the simplest and effective solution. Many users prefer the comfort of a standard Qubes OS workflow combined with the assurance that every DVM session is irrecoverably erased from disk.

This guide presents best practices for fully ephemeral DVMs, that I created for my friends. All methods described here are completely automated - no manual terminal commands are required to launch ephemeral DVMs. Dedicated systemd services handle ephemeral pool creation, DVM cloning, automatic destruction on shutdown, and seamless recreation on the next boot. The result feels like a native Qubes OS feature. In amnesic modes (overlay or zram-live), all services skips pool creation and performs only cleanup.

Four distinct approaches are presented, allowing you to select the method that best matches your threat model and hardware capabilities. But first, a reminder: for maximum anti-forensic protection and strong Qubes OS hardening, I recommend this guide.


:locked: 1. Ephemeral Encrypted Volatile DVMs (for devices up to 16 GB RAM)

Script deploys a systemd service that creates a fresh LUKS-encrypted LVM thin pool on every boot, clones all DVM templates into it as ephemeral -ephemeral variants, and completely destroys the pool - including all VM volumes, encryption keys, and the underlying image file - on shutdown. This guarantees that all DVM session data resides exclusively on encrypted volatile storage that is cryptographically irrecoverable after poweroff, leaving no forensic traces on disk.

This solution does not impose any additional memory load, which is critical for devices with less than 16 GB of RAM. My friends with 16 GB devices experienced severe issues when running DVMs entirely in RAM - DVMs would crash unexpectedly, or dom0 would start glitching: panel and app menu artifacts, cursor freezes.

  • Make sure there is sufficient free space (more than 4 GB) in dom0. You can resize dom0 these commands:
sudo lvresize --size 4G /dev/mapper/qubes_dom0-root
sudo resize2fs /dev/mapper/qubes_dom0-root
sudo lvresize -L +4G qubes_dom0/root-pool
  • Run this simple script in dom0 terminal:
#!/bin/bash

sudo tee /etc/systemd/system/ephemeral-pool.service << 'EOF'
[Unit]
Description=Fresh ephemeral encrypted LVM pool with DVM auto-clone
After=local-fs.target qubesd.service
Requires=qubesd.service
Before=qubes-vm@sys-net.service qubes-vm@sys-firewall.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStartPre=-/usr/local/bin/ephemeral-pool-destroy.sh
ExecStart=/usr/local/bin/ephemeral-pool-create.sh
ExecStop=/usr/local/bin/ephemeral-pool-destroy.sh

[Install]
WantedBy=multi-user.target
EOF

sudo tee /usr/local/bin/ephemeral-pool-create.sh << 'EOF'
#!/bin/bash
set -uo pipefail

POOL_NAME="ephemeral_pool"
MOUNT_BASE="/var/tmp/ephemeral"
KEY_FILE="/dev/shm/ephemeral.key"
IMG_FILE="${MOUNT_BASE}/pool.img"
LUKS_NAME="ephemeral_crypt"
POOL_SIZE="4G"

ROOT_DEV=$(findmnt -n -o SOURCE / 2>/dev/null || echo "")

if echo "$ROOT_DEV" | grep -qE "(overlay|/dev/zram0)"; then
    echo "[*] Detected amnesiac mode: $ROOT_DEV"
    echo "    Destroying old ephemeral VMs and artifacts..."

    for vm in $(qvm-ls --raw-list --running 2>/dev/null); do
        if qvm-volume list "$vm" 2>/dev/null | grep -q "${POOL_NAME}"; then
            echo "    -> Stopping: $vm"
            qvm-shutdown --wait --timeout 10 "$vm" 2>/dev/null || \
                qvm-kill "$vm" 2>/dev/null || true
        fi
    done

    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-remove --force "$vm" 2>/dev/null || true
        fi
    done

    qvm-pool remove "${POOL_NAME}" 2>/dev/null || true
    vgchange -an ephemeral_vg 2>/dev/null || true
    vgremove -f ephemeral_vg 2>/dev/null || true
    cryptsetup close "${LUKS_NAME}" 2>/dev/null || true
    rm -f "${KEY_FILE}" "${IMG_FILE}"

    echo "[+] Cleanup completed. Pool creation skipped in amnesiac mode."
    exit 0
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

qvm-pool remove "${POOL_NAME}" 2>/dev/null || true
sleep 1
vgchange -an ephemeral_vg 2>/dev/null || true
vgremove -f ephemeral_vg 2>/dev/null || true
cryptsetup close "${LUKS_NAME}" 2>/dev/null || true
rm -f "${KEY_FILE}" "${IMG_FILE}"

mkdir -p "${MOUNT_BASE}"
install -m600 /dev/null "${KEY_FILE}" && dd if=/dev/urandom bs=1 count=4096 of="${KEY_FILE}" iflag=fullblock
truncate -s "${POOL_SIZE}" "${IMG_FILE}"
LOOP_DEV=$(losetup -f --show "${IMG_FILE}")
cryptsetup luksFormat -q --key-file "${KEY_FILE}" "${LOOP_DEV}"
cryptsetup open --key-file "${KEY_FILE}" "${LOOP_DEV}" "${LUKS_NAME}"
pvcreate "/dev/mapper/${LUKS_NAME}"
vgcreate "ephemeral_vg" "/dev/mapper/${LUKS_NAME}"
lvcreate -T -n "thin_pool" -l +100%FREE "ephemeral_vg"
losetup -d "${LOOP_DEV}" || true

if qvm-pool list 2>/dev/null | grep -q "^${POOL_NAME}"; then
    qvm-pool remove "${POOL_NAME}" 2>/dev/null || true
    sleep 1
fi
qvm-pool add "${POOL_NAME}" lvm_thin --option volume_group=ephemeral_vg --option thin_pool=thin_pool 2>/dev/null || \
qvm-pool add "${POOL_NAME}" lvm_thin -o volume_group=ephemeral_vg,thin_pool=thin_pool

echo "[*] Copying DVM templates..."
qvm-ls --raw-list 2>/dev/null | grep -i 'dvm' | grep -v '\-ephemeral$' | while read vm; do
    [ -n "$vm" ] || continue
    clone_name="${vm}-ephemeral"
    qvm-check -q "$clone_name" 2>/dev/null && {
        qvm-kill "$clone_name" 2>/dev/null || true
        qvm-remove --force "$clone_name" 2>/dev/null || true
    }
    echo "    -> $vm -> ${clone_name}"
    qvm-clone -P "${POOL_NAME}" "$vm" "$clone_name" && {
        qvm-prefs "$clone_name" template_for_dispvms True 2>/dev/null || true
        qvm-prefs "$clone_name" autostart False 2>/dev/null || true
    }
done

echo "[+] Done!"
EOF

sudo tee /usr/local/bin/ephemeral-pool-destroy.sh << 'EOF'
#!/bin/bash
set -uo pipefail

POOL_NAME="ephemeral_pool"
LUKS_NAME="ephemeral_crypt"
KEY_FILE="/dev/shm/ephemeral.key"
IMG_FILE="/var/tmp/ephemeral/pool.img"

for vm in $(qvm-ls --raw-list 2>/dev/null); do
    if qvm-volume list "$vm" 2>/dev/null | grep -q "${POOL_NAME}"; then
        qvm-kill "$vm" 2>/dev/null || true
        sleep 1
        qvm-remove --force "$vm" 2>/dev/null || true
        sleep 1
    fi
done

qvm-pool remove "${POOL_NAME}" 2>/dev/null || true
sleep 1
qvm-pool remove "${POOL_NAME}" 2>/dev/null || true
vgchange -an ephemeral_vg 2>/dev/null || true
vgremove -f ephemeral_vg 2>/dev/null || true
cryptsetup close "${LUKS_NAME}" 2>/dev/null || true
for loop in $(losetup -j "${IMG_FILE}" 2>/dev/null | cut -d: -f1 || true); do
    [ -n "$loop" ] && losetup -d "$loop" 2>/dev/null || true
done
rm -f "${KEY_FILE}" "${IMG_FILE}"
EOF

sudo chmod +x /usr/local/bin/ephemeral-pool-create.sh
sudo chmod +x /usr/local/bin/ephemeral-pool-destroy.sh
sudo systemctl daemon-reload
sudo systemctl enable --now ephemeral-pool.service
  • To remove pool and disable the systemd service:
sudo systemctl stop ephemeral-pool.service
sudo systemctl disable ephemeral-pool.service
  • Start the service and pool again:

sudo systemctl enable --now ephemeral-pool.service


:card_index_dividers: 2. RAM-based DVMs on a Zram disk (for devices with 20-24 GB of RAM)

This script creates a compressed RAM disk (zram) that acts as a super-fast ephemeral storage pool for your DVMs. On every boot, it sets aside a portion of your RAM - compressed with lz4 algorithm to save space - builds an LVM thin pool on top of it, and clones all your DVM templates there as -ephemeral variants. When you shut down the system, everything in that RAM disk vanishes instantly without ever touching your physical disk. The key advantage of zram is built-in compression: your 3 GB pool can hold 6-7 GB of actual VM data, giving you more usable space than raw RAM would allow. It is also significantly faster than disk-based storage, so DVMs launch and run with near-native speed. Unlike tmpfs, zram does not compete with your system memory for cache.

  • First, increase the maximum dom0 memory in this file
    sudo nano /etc/default/grub
    edit this value dom0_mem=max:4096M to dom0_mem=max:6144M,
    update GRUB sudo grub2-mkconfig -o /boot/grub2/grub.cfg and reboot Qubes OS.

  • Run this simple script in dom0 terminal:
    (Also see this script in comments)

#!/bin/bash

sudo tee /etc/systemd/system/zram-pool.service << 'EOF'
[Unit]
Description=ZRAM Ephemeral Pool
After=qubesd.service
Before=qubes-vm@sys-net.service

[Service]
Type=oneshot
ExecStart=/usr/local/bin/zram-pool-create.sh
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
EOF

sudo tee /usr/local/bin/zram-pool-create.sh << 'EOF'
#!/bin/bash
set -euo pipefail

POOL_NAME="zram_pool"
ZRAM_SIZE="4G"
VG_NAME="zram_vg"

EXTRA_VMS=(
    #"whonix"
    #"sys-whonix"
)

ROOT_DEV=$(findmnt -n -o SOURCE / 2>/dev/null || echo "")

if echo "$ROOT_DEV" | grep -qE "(overlay|/dev/zram0)"; then
    echo "[*] Detected amnesiac mode: $ROOT_DEV"
    echo "    Cleaning up old VMs..."

    for vm in $(qvm-ls --raw-list --running 2>/dev/null); do
        if qvm-volume list "$vm" 2>/dev/null | grep -q "${POOL_NAME}"; then
            echo "    -> Stopping: $vm"
            qvm-shutdown --wait --timeout 10 "$vm" 2>/dev/null || \
                qvm-kill "$vm" 2>/dev/null || true
        fi
    done

    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"
            qvm-remove --force "$vm" 2>/dev/null || true
        fi
    done

    qvm-pool remove "${POOL_NAME}" 2>/dev/null || true
    vgchange -an "${VG_NAME}" 2>/dev/null || true
    vgremove -f "${VG_NAME}" 2>/dev/null || true

    for dev in /dev/zram*; do
        [ -b "$dev" ] || continue
        zramctl --reset "$dev" 2>/dev/null || true
    done

    echo "[+] Cleanup completed."
    exit 0
fi

if [ "$EUID" -ne 0 ]; then
    echo "[!] Root privileges required"
    exit 1
fi

echo "[*] Creating zram device (${ZRAM_SIZE})..."
modprobe zram 2>/dev/null || true

ZRAM_DEV=""
for dev in /dev/zram*; do
    [ -b "$dev" ] || continue
    if ! zramctl "$dev" 2>/dev/null | grep -q "mounted\|active"; then
        ZRAM_DEV="$dev"
        break
    fi
done

if [ -z "$ZRAM_DEV" ]; then
    ZRAM_DEV=$(zramctl --find --size "$ZRAM_SIZE" --algorithm lz4)
else
    zramctl --reset "$ZRAM_DEV" 2>/dev/null || true
    ZRAM_DEV=$(zramctl --find --size "$ZRAM_SIZE" --algorithm lz4)
fi

echo "    Device: $ZRAM_DEV"

echo "[*] Removing VMs from pool ${POOL_NAME}..."
for vm in $(qvm-ls --raw-list --running 2>/dev/null); do
    if qvm-volume list "$vm" 2>/dev/null | grep -q "${POOL_NAME}"; then
        echo "    -> Stopping: $vm"
        qvm-shutdown --wait --timeout 30 "$vm" 2>/dev/null || {
            echo "    -> Force killing: $vm"
            qvm-kill "$vm" 2>/dev/null || true
        }
    fi
done

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"
        qvm-remove --force "$vm" 2>/dev/null || true
    fi
done

echo "[*] Cleaning up old infrastructure..."
qvm-pool remove "${POOL_NAME}" 2>/dev/null || true
vgchange -an "${VG_NAME}" 2>/dev/null || true
vgremove -f "${VG_NAME}" 2>/dev/null || true

# Detach old loops on zram
for loopdev in $(losetup -a 2>/dev/null | grep "$ZRAM_DEV" | cut -d: -f1); do
    echo "    -> Detaching loop: $loopdev"
    losetup -d "$loopdev" 2>/dev/null || true
done

echo "[*] Creating loop on ${ZRAM_DEV}..."
LOOP_DEV=$(losetup -f --show "$ZRAM_DEV")
echo "    Loop: $LOOP_DEV"

echo "[*] Creating LVM on ${LOOP_DEV}..."
pvcreate -q "$LOOP_DEV"
vgcreate -q "${VG_NAME}" "$LOOP_DEV"
lvcreate -q -T -n "thin_pool" -l +100%FREE "${VG_NAME}"

echo "[*] Registering pool ${POOL_NAME}..."
if qvm-pool list 2>/dev/null | grep -q "^${POOL_NAME}"; then
    qvm-pool remove "${POOL_NAME}" 2>/dev/null || true
    sleep 1
fi

qvm-pool add "${POOL_NAME}" lvm_thin --option volume_group="${VG_NAME}" --option thin_pool=thin_pool 2>/dev/null || \
qvm-pool add "${POOL_NAME}" lvm_thin -o volume_group="${VG_NAME}",thin_pool=thin_pool

echo "    Pool created:"
qvm-pool info "${POOL_NAME}"

echo "[*] Cloning VMs into ephemeral pool..."

echo "    [DVM templates]"
qvm-ls --raw-list 2>/dev/null | while read -r vm; do
    [ -n "$vm" ] || continue

    is_dvm=$(qvm-prefs "$vm" template_for_dispvms 2>/dev/null || echo "False")
    [ "$is_dvm" = "True" ] || continue

    if qvm-volume list "$vm" 2>/dev/null | grep -q "${POOL_NAME}"; then
        continue
    fi

    clone_name="${vm}-ephemeral"

    if qvm-ls --raw-list 2>/dev/null | grep -q "^${clone_name}$"; then
        echo "    -> Removing old copy: ${clone_name}"
        qvm-kill "$clone_name" 2>/dev/null || true
        qvm-remove --force "$clone_name" 2>/dev/null || true
    fi

    echo "    -> Cloning: $vm -> ${clone_name}"

    if qvm-clone -P "${POOL_NAME}" "$vm" "$clone_name"; then
        qvm-prefs "$clone_name" template_for_dispvms True 2>/dev/null || true
        qvm-prefs "$clone_name" autostart False 2>/dev/null || true
        echo "    [+] OK"
    else
        echo "    [!] ERROR"
    fi
done

if [ ${#EXTRA_VMS[@]} -gt 0 ]; then
    echo "    [Extra VMs]"
    for vm in "${EXTRA_VMS[@]}"; do
        if ! qvm-ls --raw-list 2>/dev/null | grep -q "^${vm}$"; then
            echo "    [!] VM not found: $vm"
            continue
        fi

        if qvm-volume list "$vm" 2>/dev/null | grep -q "${POOL_NAME}"; then
            echo "    -> Skipping (already in zram): $vm"
            continue
        fi

        clone_name="${vm}-ephemeral"

        if qvm-ls --raw-list 2>/dev/null | grep -q "^${clone_name}$"; then
            echo "    -> Removing old copy: ${clone_name}"
            qvm-kill "$clone_name" 2>/dev/null || true
            qvm-remove --force "$clone_name" 2>/dev/null || true
        fi

        echo "    -> Cloning: $vm -> ${clone_name}"

        if qvm-clone -P "${POOL_NAME}" "$vm" "$clone_name"; then
            qvm-prefs "$clone_name" template_for_dispvms False 2>/dev/null || true
            qvm-prefs "$clone_name" autostart False 2>/dev/null || true
            echo "    [+] OK"
        else
            echo "    [!] ERROR"
        fi
    done
fi
EOF

sudo chmod +x /usr/local/bin/zram-pool-create.sh
sudo systemctl daemon-reload
sudo systemctl enable --now zram-pool.service
  • To remove zram pool and disable the systemd service run this script:
#!/bin/bash

# Step 1: Stop and disable the systemd service
sudo systemctl stop zram-pool.service
sudo systemctl disable zram-pool.service

# Step 2: Remove all VMs from zram_pool
echo "[*] Removing VMs from zram_pool..."
for vm in $(qvm-ls --raw-list 2>/dev/null); do
    if qvm-volume list "$vm" 2>/dev/null | grep -q "zram_pool"; then
        echo "    -> Removing: $vm"
        qvm-kill "$vm" 2>/dev/null || true
        sleep 1
        qvm-remove --force "$vm" 2>/dev/null || true
        sleep 1
    fi
done

# Step 3: Remove the Qubes storage pool
echo "[*] Removing zram_pool..."
qvm-pool remove zram_pool 2>/dev/null || true

# Step 4: Deactivate and remove LVM volume group
echo "[*] Cleaning up LVM..."
vgchange -an zram_vg 2>/dev/null || true
vgremove -f zram_vg 2>/dev/null || true

# Step 5: Detach loop device from zram
echo "[*] Detaching loop devices..."
for loopdev in $(losetup -a 2>/dev/null | grep "/dev/zram" | cut -d: -f1); do
    echo "    -> Detaching: $loopdev"
    losetup -d "$loopdev" 2>/dev/null || true
done

# Step 6: Reset zram device
echo "[*] Resetting zram device..."
for dev in /dev/zram*; do
    [ -b "$dev" ] || continue
    zramctl --reset "$dev" 2>/dev/null || true
done

echo "[+] zram pool completely removed. Reboot to clear all traces from memory."
  • Start the service and pool again:

sudo systemctl enable --now zram-pool.service


:tornado: 3. RAM-based DVMs on TMPFS (for devices with 32 GB of RAM)

This variant creates a pure RAM-based ephemeral pool using tmpfs. On every boot, it allocates a portion of your system memory as a temporary filesystem, builds an LVM thin pool inside it, and clones all your DVM templates there as ephemeral variants. When you shut down or reboot, the entire pool and all its VMs vanish instantly. Direct RAM access means DVMs launch faster than any other storage type. No encryption overhead, no zram compression delays or disk I/O bottlenecks - just RAM and LVM. The entire pool size is reserved from RAM immediately, making this best suited for systems with 32 GB or more.

  • First, increase the maximum dom0 memory in this file
    sudo nano /etc/default/grub
    edit this value dom0_mem=max:4096M to dom0_mem=max:8192M,
    update GRUB sudo grub2-mkconfig -o /boot/grub2/grub.cfg and reboot Qubes OS.

  • Run this simple script in dom0 terminal:

#!/bin/bash

sudo tee /etc/systemd/system/tmpfs-pool.service << 'EOF'
[Unit]
Description=TMPFS Ephemeral Pool
After=qubesd.service
Before=qubes-vm@sys-net.service

[Service]
Type=oneshot
ExecStart=/usr/local/bin/tmpfs-pool-create.sh
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
EOF

sudo tee /usr/local/bin/tmpfs-pool-create.sh << 'EOF'
#!/bin/bash
set -uo pipefail

POOL_NAME="tmpfs_pool"
MOUNT_BASE="/dev/shm/ephemeral"
IMG_FILE="${MOUNT_BASE}/pool.img"
POOL_SIZE="4G"
VG_NAME="ephemeral_vg"

ROOT_DEV=$(findmnt -n -o SOURCE / 2>/dev/null || echo "")

if echo "$ROOT_DEV" | grep -qE "(overlay|/dev/zram)"; then
    echo "[*] Detected amnesiac mode: $ROOT_DEV"
    echo "    Destroying old ephemeral VMs and artifacts..."

    for vm in $(qvm-ls --raw-list --running 2>/dev/null); do
        if qvm-volume list "$vm" 2>/dev/null | grep -q "${POOL_NAME}"; then
            echo "    -> Stopping: $vm"
            qvm-shutdown --wait --timeout 10 "$vm" 2>/dev/null || \
                qvm-kill "$vm" 2>/dev/null || true
        fi
    done

    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"
            qvm-remove --force "$vm" 2>/dev/null || true
        fi
    done

    qvm-pool remove "${POOL_NAME}" 2>/dev/null || true
    vgchange -an "${VG_NAME}" 2>/dev/null || true
    vgremove -f "${VG_NAME}" 2>/dev/null || true
    rm -rf "${MOUNT_BASE}"

    echo "[+] Cleanup completed. Pool creation skipped in amnesiac mode."
    exit 0
fi

echo "[*] Phase 1: Removing ALL VMs from ephemeral pool..."

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

# Pool in RAM — simply delete files
echo "[*] Phase 2: Cleaning old pool from RAM..."

qvm-pool remove "${POOL_NAME}" 2>/dev/null || true
vgchange -an "${VG_NAME}" 2>/dev/null || true
vgremove -f "${VG_NAME}" 2>/dev/null || true

# Remove old files (if any remain)
rm -rf "${MOUNT_BASE}"

echo "[*] Phase 3: Creating fresh ephemeral pool in RAM..."

mkdir -p "${MOUNT_BASE}"

# New image in RAM
truncate -s "${POOL_SIZE}" "${IMG_FILE}"

# Loop device
LOOP_DEV=$(losetup -f --show "${IMG_FILE}")

# LVM thin pool directly on loop (no encryption)
pvcreate "$LOOP_DEV"
vgcreate "${VG_NAME}" "$LOOP_DEV"
lvcreate -T -n "thin_pool" -l +100%FREE "${VG_NAME}"

# Detach loop
losetup -d "${LOOP_DEV}" || true

echo "[*] Phase 4: Registering pool..."

if qvm-pool list 2>/dev/null | grep -q "^${POOL_NAME}"; then
    qvm-pool remove "${POOL_NAME}" 2>/dev/null || true
    sleep 1
fi

qvm-pool add "${POOL_NAME}" lvm_thin --option volume_group="${VG_NAME}" --option thin_pool=thin_pool 2>/dev/null || \
qvm-pool add "${POOL_NAME}" lvm_thin -o volume_group="${VG_NAME}",thin_pool=thin_pool

if ! qvm-pool list 2>/dev/null | grep -q "^${POOL_NAME}"; then
    echo "[!] Pool registration failed"
    exit 1
fi

echo "[+] Pool ready in RAM"

echo "[*] Phase 5: Copying DVM templates..."

echo "    [DVM templates]"
qvm-ls --raw-list 2>/dev/null | while read -r vm; do
    [ -n "$vm" ] || continue

    is_dvm=$(qvm-prefs "$vm" template_for_dispvms 2>/dev/null || echo "False")
    [ "$is_dvm" = "True" ] || continue

    if qvm-volume list "$vm" 2>/dev/null | grep -q "${POOL_NAME}"; then
        continue
    fi

    clone_name="${vm}-ephemeral"

    if qvm-ls --raw-list 2>/dev/null | grep -q "^${clone_name}$"; then
        echo "    -> Removing old copy: ${clone_name}"
        qvm-kill "$clone_name" 2>/dev/null || true
        qvm-remove --force "$clone_name" 2>/dev/null || true
    fi

    echo "    -> Cloning: $vm -> ${clone_name}"

    if qvm-clone -P "${POOL_NAME}" "$vm" "$clone_name"; then
        qvm-prefs "$clone_name" template_for_dispvms True 2>/dev/null || true
        qvm-prefs "$clone_name" autostart False 2>/dev/null || true
        echo "    [+] OK"
    else
        echo "    [!] ERROR"
    fi
done

echo "[+] Done!"
echo ""
echo "VMs in ephemeral pool:"
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 "  - $vm"
    fi
done
EOF

sudo chmod +x /usr/local/bin/tmpfs-pool-create.sh
sudo systemctl daemon-reload
sudo systemctl enable --now tmpfs-pool.service
  • To remove zram pool and disable the systemd service run this script:
#!/bin/bash

# Step 1: Stop the service (prevents auto-restart on boot)
sudo systemctl stop tmpfs-pool.service
sudo systemctl disable tmpfs-pool.service

# Step 2: Remove all VMs from tmpfs_pool
echo "[*] Removing VMs from tmpfs_pool..."
for vm in $(qvm-ls --raw-list 2>/dev/null); do
    if qvm-volume list "$vm" 2>/dev/null | grep -q "tmpfs_pool"; then
        echo "    -> Removing: $vm"
        qvm-kill "$vm" 2>/dev/null || true
        sleep 1
        qvm-remove --force "$vm" 2>/dev/null || true
        sleep 1
    fi
done

# Step 3: Remove the Qubes storage pool
echo "[*] Removing tmpfs_pool..."
qvm-pool remove tmpfs_pool 2>/dev/null || true

# Step 4: Clean up LVM
echo "[*] Cleaning up LVM..."
vgchange -an ephemeral_vg 2>/dev/null || true
vgremove -f ephemeral_vg 2>/dev/null || true

# Step 5: Free RAM by removing the image
echo "[*] Freeing RAM..."
rm -rf /dev/shm/ephemeral

echo "[+] tmpfs pool stopped and cleaned up."
  • Start the service and pool again:

sudo systemctl enable --now tmpfs-pool.service


:gear: 4. Modifying default DVMs

This solution is for those who want to configure the default DVMs for amnesic operation - without creating new pools or new DVMs.

  • Сreate a systemd service that remounts the root in the DVM as an ephemeral encrypted volatile volume:
#!/bin/bash

sudo tee /etc/systemd/system/dvm-root.service << 'EOF'
[Unit]
Description=DVM root rw False
After=qubesd.service
Requires=qubesd.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/dvm-root.sh

[Install]
WantedBy=multi-user.target
EOF

sudo tee /usr/local/bin/dvm-root.sh << 'EOF'
#!/bin/bash
set -euo pipefail

while read -r vm; do
    [ -n "$vm" ] || continue
    is_dvm=$(qvm-prefs "$vm" template_for_dispvms 2>/dev/null || echo "False")
    [ "$is_dvm" = "True" ] || continue
    qvm-volume config "${vm}:root" rw false
done < <(qvm-ls --raw-list)
EOF

sudo chmod +x /usr/local/bin/dvm-root.sh
sudo systemctl daemon-reload
sudo systemctl enable dvm-root.service
qvm-pool set vm-pool -o ephemeral_volatile=True
  • Then restart Qubes OS.
  • Then increase memory for all your DVMs in the Qube Manager: initial memory: 1000, max memory: 5000.
  • Then add this script to /rw/config/rc.local in DVM (this creates overlay in RAM for /home)

(If you want to make changes to DVM templates, simply remove the script from /rw/config/rc.local, restart the DVM template, apply your changes, and add the script back to /rw/config/rc.local)

TMPFS Script for Debian/Fedora/Kicksecure-based DVM templates:

OVERLAY_BASE="/run/home-overlay"
LOWERDIR="/home"
UPPERDIR="$OVERLAY_BASE/upper"
WORKDIR="$OVERLAY_BASE/work"
MOUNTPOINT="/home"

# Create directories
mkdir -p "$UPPERDIR" "$WORKDIR"

# Mount tmpfs for overlay upper+work layers
mount -t tmpfs -o size=2G,mode=0755 tmpfs "$OVERLAY_BASE"

# Recreate upper/work inside tmpfs after mount
mkdir -p "$UPPERDIR" "$WORKDIR"

# Mount overlayfs
mount -t overlay overlay \
  -o lowerdir="$LOWERDIR",upperdir="$UPPERDIR",workdir="$WORKDIR" \
  "$MOUNTPOINT"

Or alternative Script: Zram-disk for Debian/Fedora/Kicksecure-based DVM templates (If you want an overlay with memory compression - saves RAM, but adds slight CPU overhead):

ZRAM_DEV=""
ZRAM_SIZE="2G"
LOWERDIR="/home"
OVERLAY_BASE="/run/home-overlay"
UPPERDIR="$OVERLAY_BASE/upper"
WORKDIR="$OVERLAY_BASE/work"
MOUNTPOINT="/home"

# --- 1. Create and configure zram device ---

# Load zram module if not loaded
if ! lsmod | grep -q "^zram"; then
    modprobe zram num_devices=1 || modprobe zram
fi

# Wait for zram control interface
for _ in {1..10}; do
    if [ -d /sys/class/zram-control ]; then
        break
    fi
    sleep 0.1
done

# Find first free zram device
for i in /sys/block/zram*; do
    [ -e "$i" ] || continue
    if [ "$(cat "$i"/disksize)" = "0" ]; then
        ZRAM_DEV="/dev/$(basename "$i")"
        break
    fi
done

if [ -z "$ZRAM_DEV" ]; then
    # No free device; try to add one via hot_add
    if [ -f /sys/class/zram-control/hot_add ]; then
        idx=$(cat /sys/class/zram-control/hot_add)
        ZRAM_DEV="/dev/zram$idx"
    else
        echo "ERROR: no free zram device found and hot_add not available" >&2
        exit 1
    fi
fi

echo "Using $ZRAM_DEV"

# Configure compression and size
echo lz4 > /sys/block/$(basename "$ZRAM_DEV")/comp_algorithm 2>/dev/null || true
echo "$ZRAM_SIZE" > /sys/block/$(basename "$ZRAM_DEV")/disksize

# Format and mount zram as ext4 (needed for overlay upper+work)
mkfs.ext4 -q "$ZRAM_DEV"

mkdir -p "$OVERLAY_BASE"
mount -t ext4 "$ZRAM_DEV" "$OVERLAY_BASE"

# --- 2. Prepare overlay directories ---

mkdir -p "$UPPERDIR" "$WORKDIR"

# --- 3. Mount overlayfs ---

mount -t overlay overlay \
  -o lowerdir="$LOWERDIR",upperdir="$UPPERDIR",workdir="$WORKDIR" \
  "$MOUNTPOINT"

Script for Whonix-based DVM templates:

for i in {1..15}; do
if [ -b /dev/xvdc ] && mountpoint -q /volatile 2>/dev/null; then
break
fi
done

if ! mountpoint -q /volatile 2>/dev/null; then
mount /dev/xvdc /volatile || echo "Volatile mount failed"
fi

#
remount_dir() {
local dir="$1"
local volatile_dir="/volatile$dir"

mkdir -p "$volatile_dir"

[ -z "$(ls -A "$volatile_dir" 2>/dev/null)" ] && cp -a "/rw$dir/." "$volatile_dir/" 2>/dev/null || true
umount -l "$dir" 2>/dev/null || true
mount --bind "$volatile_dir" "$dir" || echo "Bind $dir failed"
}

#
mkdir -p /volatile/home
cp -a /rw/home/. /volatile/home/ 2>/dev/null || true
umount -l /home 2>/dev/null || true
mount --bind /volatile/home /home
remount_dir "/var/spool/cron"
remount_dir "/usr/local"
remount_dir "/var/lib/systemcheck"
remount_dir "/var/lib/canary"
remount_dir "/var/cache/setup-dist"
remount_dir "/var/lib/sdwdate"
remount_dir "/var/lib/dummy-dependency"
remount_dir "/var/cache/anon-base-files"
remount_dir "/var/lib/whonix"
USER_NAME="user"

USER_HOME="/home/$USER_NAME"
LOCAL_APP_DIR="$USER_HOME/.local/share/applications"
SYSTEM_DESKTOP="/usr/share/applications/pcmanfm-qt.desktop"
USER_DESKTOP="$LOCAL_APP_DIR/pcmanfm-qt.desktop"

mkdir -p "$LOCAL_APP_DIR"

if [ -r "$SYSTEM_DESKTOP" ]; then
if [ ! -e "$USER_DESKTOP" ]; then
cp "$SYSTEM_DESKTOP" "$USER_DESKTOP"
fi

sed -i "s|^Exec=.*|Exec=pcmanfm-qt $USER_HOME|" "$USER_DESKTOP"
fi

QTERMINAL_USER="$LOCAL_APP_DIR/qterminal.desktop"
if [ -r "/usr/share/applications/qterminal.desktop" ]; then
    cp "/usr/share/applications/qterminal.desktop" "$QTERMINAL_USER"
    # bash -c с cd!
    sed -i "0,/^Exec=/s|^Exec=.*|Exec=bash -c 'cd /home/user \&\& exec qterminal'|" "$QTERMINAL_USER"
fi

:shower: :toilet: Additional anti-forensic protection - volatile logs and metadata.

For additional protection, you can use this script and redirect all system journal logs to memory. This script configures tmpfs mounts for /var/log, /etc/lvm/archive, /etc/lvm/backup, and /var/lib/qubes/backup in /etc/fstab and creates /etc/tmpfiles.d/tmpfs.conf to ensure required directories exist on boot. Additionally, this script deletes files containing metadata about removed VMs (empty disp-directories /home/user/.local/share/qubes-appmenus/):

#!/bin/bash

set -euo pipefail

# ============================================================================
# Step 1: Add tmpfs entries to /etc/fstab
# ============================================================================

FSTAB_ENTRIES=(
    "tmpfs /var/log tmpfs defaults,noatime,size=50M 0 0"
    "tmpfs /etc/lvm/archive tmpfs defaults,size=50M,noatime 0 0"
    "tmpfs /etc/lvm/backup tmpfs defaults,size=50M,noatime 0 0"
    "tmpfs /var/lib/qubes/backup tmpfs defaults,size=20M,noatime 0 0"
)

for entry in "${FSTAB_ENTRIES[@]}"; do
    mount_point=$(echo "$entry" | awk '{print $2}')
    if grep -q "^[[:space:]]*tmpfs[[:space:]]\+${mount_point}[[:space:]]" /etc/fstab; then
        echo "[SKIP] tmpfs mount for $mount_point already exists in /etc/fstab"
    else
        printf '\n%s\n' "$entry" >> /etc/fstab
        echo "[ADDED] $entry"
    fi
done

# ============================================================================
# Step 2: Create /etc/tmpfiles.d/tmpfs.conf
# ============================================================================

TMPFILES_CONF="/etc/tmpfiles.d/tmpfs.conf"

cat > "$TMPFILES_CONF" << 'EOF'
# /etc/tmpfiles.d/tmpfs.conf
# Ensures directories exist on boot for tmpfs-backed mounts.

# /var/log subdirectories
d /var/log/qubes           2770 root qubes          -
d /var/log/audit           700  root root            -
d /var/log/xen             770  root qubes          -
d /var/log/xen/console     2750 root qubes          -
d /var/log/anaconda        755  root root            -
d /var/log/samba           700  root root            -
d /var/log/samba/old       700  root root            -
d /var/log/blivet-gui      755  root root            -
d /var/log/usbguard        755  root root            -
d /var/log/lightdm         755  lightdm lightdm     -
d /var/log/journal         2755 root systemd-journal -
d /var/log/private         700  root root            -
d /var/log/libvirt         700  root root            -
d /var/log/libvirt/libxl   700  root root            -
d /var/log/salt            755  root root            -

# LVM metadata directories
d /etc/lvm/archive         755  root root -
d /etc/lvm/backup          755  root root -

# Qubes backup directory
d /var/lib/qubes/backup    755  root root -
EOF

# ============================================================================
# Step 3: Create systemd cleanup service
# ============================================================================

sudo tee /etc/systemd/journald.conf << EOF
[Journal]
Storage=volatile
EOF
sudo systemctl restart systemd-journald

sudo tee /etc/systemd/system/clean.service << 'EOF'
[Unit]
Description=Clean logs of removed Qubes VMs
After=qubesd.service

[Service]
Type=oneshot
ExecStart=/usr/local/bin/clean.sh
RemainAfterExit=no

[Install]
WantedBy=multi-user.target
EOF

sudo tee /usr/local/bin/clean.sh << 'EOF'
#!/bin/bash

#!/bin/bash

set -euo pipefail


readonly TEMPDIR_ROOT='/home/user/tmp'
readonly MENUDIR='/home/user/.config/menus/applications-merged'


existing_qubes=$(qvm-ls --fields=name --raw-data | sort)


all_qube_names=''


if [ -d "${TEMPDIR_ROOT}" ]; then
    all_qube_names+=$(find "${TEMPDIR_ROOT}" \
        -mindepth 1 -maxdepth 1 -type d \
        -exec basename "{}" \; \
        | sort | uniq)$'\n'
fi


all_qube_names+=$(find "${MENUDIR}" \
    -regextype posix-egrep \
    -regex '.*\/user-qubes-.*\.menu$' \
    -exec basename "{}" \; \
    | sed -r 's/\.menu$//g' \
    | sed -r 's/^user-qubes-(disp)?vm-directory(_|-)//g' \
    | sort | uniq)$'\n'


all_qube_names=$(echo "${all_qube_names}" \
    | sed -r 's/^(Domain-0|libxl-driver)$//g' \
    | sed -r '/^\s*$/d' \
    | sort | uniq)


set +e
qubes_to_remove=$(diff --new-line-format='' --unchanged-line-format='' \
    <(echo "${all_qube_names}") <(echo "${existing_qubes}") \
    | sed -r '/^\s*$/d')
set -e


for qube_name in ${qubes_to_remove}; do
    decoded_qube_name=$(echo "${qube_name}" \
        | sed -r 's/_d/-/g' \
        | sed -r 's/_u/_/g')

    log_pattern="${qube_name}\.log((\.old)|(-[0-9]{8}))?(\.gz)?"
    menu_pattern="user-qubes-(disp)?vm-directory(_|-)${qube_name}\.menu"

    declare -A targets
    targets=(["${MENUDIR}"]="${menu_pattern}")

    if [ -d "${TEMPDIR_ROOT}" ]; then
        targets+=("${TEMPDIR_ROOT}"="${qube_name}")
    fi

    for search_dir in "${!targets[@]}"; do
        mapfile -d $'\0' found_files < <(find "${search_dir}" \
            -regextype posix-egrep \
            -regex ".*\/${targets[${search_dir}]}$" \
            -print0)
        for file in "${found_files[@]}"; do
            [ -z "${file}" ] && continue
            rm -rf "${file}"
        done
    done
done
EOF

sudo chmod +x /usr/local/bin/clean.sh
sudo systemctl daemon-reload
sudo systemctl enable clean.service

echo ""
echo "Done. Reboot to apply tmpfs mounts"

However, keep in mind that VM startup metadata is written to disk from these two directories:

/etc/libvirt/libxl
/var/lib/qubes

and mounting them as tmpfs will break Qubes functionality. Therefore, for maximum 100% anti-forensic protection, use this guide


Also see these my guides:

3 Likes

Love it, this one I really like. Two suggestions: why not just turn off logging in dom0? Deleting logs is good, disabling logs is better. Also PLEASE put your scripts into a github repo xD

1 Like

At the end of the guide, there’s a script for complete metadata removal and moving logs to memory. Besides logs, there was also the metadata leak issue that’s been discussed on github for a long time. This script fixes this issue.

I will think about it :slightly_smiling_face:

1 Like

Which metadata leak issue?

My general problem with your approaches is that its “hidden” while it kinda isn’t. This all would be much cooler if there were no way to determine that its there.

Paranoid encryption (even encryption within encryption, which I’m not a fan of) and alike is nice, but there is always this “hit him with this 5 usd wrench until he gives us the password” problem - from what I can tell you are from a country where all your guides are actually relevant to protect your security, so I’m sure you are familiar with that threat model.

I love your guide on in-ram dom0, but I don’t fully trust this to use in production bcs its not in a git repo and I can’t track changes you make (sorry to bitch around, but this is actually important to me to use it in production).

I’m writing kuhbs right now, an enduser focused config management system and appstore if you will, and I’ve been eyeballing your threads really hard to see what of your work I could use there..

What I’d love to have would be a “hidden” and ephemerally encrypted space for VMs / their data, but then it actually has to be hidden.

Also that “/boot and luks header on USB” would be sth I’d love to ship right away.. But that I can work on too (not as a mandatory feature, but as optional feature).

So yeah, bla bla bla, all in all I love your work - please take one further step and start using proper code management for it. Your guides are super helpful to a lot of users, and putting them on github so we can track changes is very important I think. I’d be super happy if you could make that step.

Really great work btw, love your guides, massive talent here, keep going :slight_smile:

I already sent you the guide for it in another comment here. I think it’s a great guide and you should give it a try.

1 Like

No I need that to work AFTER the installation, as in modify an existing Qubes install. As I replied to you in that thread :stuck_out_tongue: I need all of this to work for endusers (also I have little interest in messing with Qubes installer myself tbh, I am technically capable of it but find it annoying / a not KISS enough approach). Your approach is technically cleaner of course…

With my approach (MOVING /boot and luks header somewhere else) you could actually use that 600 MB free space for something funny, like flashing a decoy OS to it, something like alpine or whatever xD Then if you turn on the laptop without the USB stick, it just boots alpine or sth really tiny. Like some live-system or so. Would make it look like the “experimenting around” laptop.

My first ever Linux project (jesus its 20 years ago) for a company was sth I called “cryptokey” - they wanted to encrypt their linux workstations while not having their developers remember a password - so I setup a script in initramfs that detects if a specific USB stick is inserted, and then from a hardcoded offset loads a binary decryption key. I left like 1GB space for the key to be somewhere, and the rest was a fat32 partition so they could use it as a normal USB stick. This worked really well, and if you found the USB stick you would think its just a USB stick with sbd to stupid to make the partition start at the beginning of the stick.

Your work reminds me of a young myself sometimes x)

Okay, I will look it

1 Like

Got a link?

2 Likes

I thought about opening a Feedback > Forum Feedback topic about the long community guide titles, but I would like to bring this here first. I don’t like the long titles with emojis, it clutters the index of new posts and I can’t help but see them, so I end up muting the topic. Am I the only one? If so, just ignore :slight_smile:

1 Like

I think @linuxuser1 is trying to SEO his threads using long topic lines. I don’t think that’s a bad thing, just visually unappealing.

Personally I don’t mind.

1 Like

It’s really awesome when guides publish scripts. No need to manually enter 20 commands) And it’s great when everything works with a gui. Thanks for the fantastic guides!

1 Like

@linuxuser1

When you publish something as part of “my guides” and you include code that someone else has written for purposes it was never aimed to serve, it is guiding readers into believing things that are not true, more specifically:

What makes it additionally dangerous is that:

  • You have capitalized some variable names - a practice that is recommended against for good reasons.

  • You have removed the important warnings (the whole TUI) in order to automate running sudo rm -rf in dom0 without a single note.

  • All that comes under a bombastic title with promises about “most effective” and what not

It is up to anyone to decide for oneself what to run in their dom0. I just wanted to mention these little details.

1 Like

as the bri-ish would say: bit harsh.

Yes, I really forgot to include a link to your metadata cleaning guide. I’m fixing this now. It was such a lengthy guide that I wanted to finish it more quickly. So I apologize. I even mixed up one script in the first version. I’ve been using this automatic cleanup for a very long time across different Qubes versions and it works great, including old logs cleanup.

Next time I will make it “extremely medium” :slight_smile:

There is no fix. The script was created to keep the system clean, and that has nothing to do with anti-forensics. It only removes files from the filesystem. It does not remove traces of data from storage, especially SSD.

1 Like

If it weren’t for the volatile journal, I wouldn’t have added this script at all. But cleanliness is also useful, especially on NVMe drives with discard enabled. It’s a debate about terminology and probabilities. Personally, I believe that volatile journal + file removing with discard SSD can definitely be called an anti-forensic tool. But okay, specially for you and out of respect for you, I will edit the description of this section, specifying that simply deleting files in /var/log is not direct anti-forensics.

1 Like

Is there a reason you use qvm-ls --raw-list 2>/dev/null | grep -q "^${clone_name}$" instead of qvm-check -q "$clone_name"?

1 Like

When you write your key you write with dd then change permissions. Firstly, I think this is fairly useless since it is in dom0. Regardless, this means the full key is written and only then are permissions changed. So instead, create the file with the right permissions and then write your data: install -m600 /dev/null “$key_file” && dd if=/dev/urandom bs=1 count=4096 of=”$key_file” iflag=fullblock

1 Like