All my scripts with License:Unlicense
This guide solves the old problems:
- DisposableVMs: support for in-RAM execution only (for anti-forensics
- Reduce leakage of disposable VM content and history into dom0 filesystem
- Opt-out logs cleanup on qube removal
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.
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
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 valuedom0_mem=max:4096Mtodom0_mem=max:6144M,
update GRUBsudo grub2-mkconfig -o /boot/grub2/grub.cfgand 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
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 valuedom0_mem=max:4096Mtodom0_mem=max:8192M,
update GRUBsudo grub2-mkconfig -o /boot/grub2/grub.cfgand 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
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.localin 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
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:
- 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
- Encrypted AppVMs and Templates. Encrypted pool. Secret vault in a LUKS-Pool. Paranoid security
- Alternative duress passwords for selective VMs destruction or system destruction in coercive environments. Paranoid security and privacy. Anti-forensics
- Awesome Overlay: overlayfs on tmpfs, zram block device. and plain dm-crypt. Ephemerality for VMs and directories
- 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