I backup my templates and AppVMs occasionally via the Qubes backup tool but I would like to automate a more frequent cloud backup solution for some of my important files within my AppVMs. What I’m thinking is that I will create a centralised qube with access to cloud storage via Cryptomator. And I will somehow copy my important files from their respective AppVMs to the “Cryptomator” AppVM which will then back them up to cloud storage. This compartmentalises Cryptomator and the cloud storage apps to one place.
I’m wondering if there are any suggestions on the best way to get these files from their respective AppVM to the “Cryptomator” AppVM. I’d rather not punch holes in firewalls and have the qubes communicating directly via tcp so I’m thinking the most secure way would be to make use of qvm-copy, perhaps staging via ~/QubesIncoming and then onto the cloud storage from there. But it doesn’t seem very efficient.
Is there a better way? What about some kind of functionality similar to sharing a USB device from sys-usb to AppVM that might allow me to share a mount point from cryptomator-vm to AppVM?
Alternative approaches would be to use rsync or syncthing over qrexec -
syncthing would provide a completely automated solution, which seems to
match your requirements.
I salt syncthing here with the packaged version here. That provides a syncthing
server to access external nodes, and a syncthing qubes service that you
can enable between qubes.
If you only have a few files then a scripted qvm-copy would do - if you
have more than a few, and they change over time, then an automated
system would be better.
If you want advice on setting up one of these services just ask.
I never presume to speak for the Qubes team.
When I comment in the Forum I speak for myself.
Thank you @whoami and @solene - this seems like a lightweight solution that might suit my needs perfectly well.
Thanks @unman - you’ve given me plenty of topics to research there as I’m not familiar with any of them. I think I’m going to put in place the piped qvm-run solution immediately for a few essential files and then read up on your suggestions as a learning opportunity if nothing else.
Thanks - I haven’t although this particular backup requirement is to backup certain user files to cloud storage in a manner which allows them to be retrieved and used with very few tools. I’m thinking in cases where my PC blows up and I have to use another device until I can replace it. The other solutions seem to meet those needs for now though.
User-Friendly: GUI tools and no salt for the god sake. My instinct tells me to use gocryptfs and megacmd or rclone with integration scripts. Yes I think in a sophisticated and elagant setup with Nextcloud.
Compartmentalization: separation between vault and cloud operations.
Air-Gapped Encryption in Vault: before leaving the secure vault.
Client-Side Encryption: MEGA only receives already-encrypted vault files.
PROTOTYPE
Necessary fixes or improvements
Corretly and secure handle the user space (appvm) and the root space (template)
User space (appvm) and the root space (template) are breaking my mind
Restric the RPC policies and maybe create variables at the top of the script for user choice
Restric the user-bind
Long tests with daily tasks
qubes-gui-cloud-backup.sh
#!/bin/bash
################################################################################
# File Name : qubes-gui-cloud-backup.sh
# Description : User-friendly encrypted cloud backup with Cryptomator GUI and
# MEGA GUI. Provides seamless integration between air-gapped
# vault management and cloud synchronization.
# Usage : • Transfer to dom0:
# qvm-run -p appvm 'cat ~/qubes-gui-cloud-backup.sh' > ~/qubes-gui-cloud-backup.sh
# • Make executable:
# chmod +x ~/qubes-gui-cloud-backup.sh
# • Run:
# bash ~/qubes-gui-cloud-backup.sh
# Author : Me and thebois
# License : Free of charge, no warranty
# Last edited : 2025-11-11
################################################################################
# Safety first
set -euo pipefail
# Configuration
BASE_TEMPLATE="debian-12-minimal"
VAULT_TEMPLATE="debian-vault-template"
CLOUD_TEMPLATE="debian-cloud-template"
VAULT_APPVM="appvm-vault"
CLOUD_APPVM="appvm-cloud"
CLOUD_APPVM_NETVM="sys-whonix"
PRIV_VOL_SIZE="15000000000" # 15GB for vaults
CRYPTOMATOR_URL="https://api.github.com/repos/cryptomator/cryptomator/releases/latest"
MEGA_BASE_URL="https://mega.nz/linux/repo/Debian_12/"
MEGA_VER_URL="https://mega.nz/linux/repo/Debian_12/Packages"
# Step 1: Verify and create base template
create_base_template() {
echo -e "\nStep 1: Verifying and creating: $BASE_TEMPLATE..."
if ! qvm-check "$BASE_TEMPLATE"; then
echo -e "\nInstalling $BASE_TEMPLATE..."
sudo qubes-dom0-update "qubes-template-$BASE_TEMPLATE"
fi
# Ensure template is shut down
qvm-shutdown --wait "$BASE_TEMPLATE" 2>/dev/null || true
if qvm-check "$BASE_TEMPLATE"; then
echo -e "\nUpdating $BASE_TEMPLATE..."
sudo qubesctl --show-output --skip-dom0 --targets="$BASE_TEMPLATE" state.sls update.qubes-vm
fi
echo -e "\nBase template setup complete"
}
# Configure bind-dirs for persistence
configure_bind_dirs() {
local template="$1"
local app_name="$2"
echo "Configuring bind-dirs for $app_name in $template..."
qvm-run -p -u root "$template" "
# Create bind-dirs configuration directory
mkdir -p /rw/config/qubes-bind-dirs.d
# Create configuration file
cat > /rw/config/qubes-bind-dirs.d/50_${app_name}.conf </dev/null; then
echo "Creating Vault Template..."
qvm-clone "$BASE_TEMPLATE" "$VAULT_TEMPLATE"
qvm-prefs "$VAULT_TEMPLATE" label black
fi
qvm-start --skip-if-running "$VAULT_TEMPLATE"
# Configure bind-dirs first
configure_bind_dirs "$VAULT_TEMPLATE" "cryptomator"
# Set environment
qvm-run -p -u root "$VAULT_TEMPLATE" "echo 'TERM=xterm' >> /etc/environment"
qvm-run -p -u root "$VAULT_TEMPLATE" "sed -i 's/^# *\(en_US.UTF-8\)/\1/' /etc/locale.gen"
qvm-run -p -u root "$VAULT_TEMPLATE" "locale-gen en_US.UTF-8"
# Install dependencies IN TEMPLATE (persistent)
qvm-run -p -u root "$VAULT_TEMPLATE" "
apt-get install -y --no-install-recommends \
qubes-core-agent-networking \
qubes-core-agent-passwordless-root \
curl \
fuse3 \
libasound2 \
libxtst6 \
thunar \
qubes-core-agent-thunar \
mousepad \
less \
psmisc \
xfce4-terminal
"
# Create mimeapps.list with default MIME type associations
local temp_mime_file=$(mktemp)
cat > "$temp_mime_file" < "$temp_crypto_script" <&2
exit 1
fi
# Extract the correct x86_64 AppImage URL
VERSION_URL=$(echo "$RESPONSE" | grep -o '"browser_download_url": *"[^"]*"' | grep "x86_64.*AppImage" | cut -d '"' -f 4 | head -n 1)
# Show what URL was found
echo "Found Cryptomator URL: $VERSION_URL"
# If no URL was found, exit with error
if [[ -z "$VERSION_URL" ]]; then
echo "ERROR: No valid x86_64 AppImage URL found for $APP_NAME." >&2
echo "Available browser_download_url entries:" >&2
echo "$RESPONSE" | grep -o '"browser_download_url": *"[^"]*"' >&2
exit 1
fi
echo "Downloading $APP_NAME from $VERSION_URL..."
# Download as root to persistent location
if ! curl --tlsv1.2 -x http://127.0.0.1:8082/ -L -o "$INSTALL_DIR/cryptomator.AppImage" "$VERSION_URL"; then
echo "ERROR: Download failed." >&2
exit 1
fi
# Verify file was actually downloaded
if [[ ! -f "$INSTALL_DIR/cryptomator.AppImage" ]]; then
echo "ERROR: Download completed but file not found!" >&2
exit 1
fi
echo "Download completed successfully. File size: $(du -h "$INSTALL_DIR/cryptomator.AppImage" | cut -f1)"
# Hash verification
echo "Verifying integrity..."
# Extract the hash from API
ASSET_NAME=$(basename "$VERSION_URL")
# Extract hash
EXPECTED_HASH=$(echo "$RESPONSE" | \
grep -A 30 "\"name\": *\"$ASSET_NAME\"" | \
grep '"digest"' | \
head -n 1 | \
sed -n 's/.*"digest": *"sha256:\([a-f0-9]*\)".*/\1/p')
if [[ -z "$EXPECTED_HASH" ]]; then
echo "ERROR: Could not extract hash from release info!" >&2
echo "Looking for asset: $ASSET_NAME" >&2
echo "Available assets with digests:" >&2
echo "$RESPONSE" | grep -B 5 -A 5 '"digest"' >&2
rm -f "$INSTALL_DIR/cryptomator.AppImage"
exit 1
fi
echo "Expected hash: $EXPECTED_HASH"
ACTUAL_HASH=$(sha256sum "$INSTALL_DIR/cryptomator.AppImage" | cut -d' ' -f1)
echo "Found hash: $ACTUAL_HASH"
if [[ "$EXPECTED_HASH" != "$ACTUAL_HASH" ]]; then
echo "ERROR: Hash verification failed!" >&2
echo "Hashes do not match!" >&2
rm -f "$INSTALL_DIR/cryptomator.AppImage"
exit 1
fi
echo "Hash verification passed"
# Set permissions (for user execution in appvm and root ownership in template)
chmod 755 "$INSTALL_DIR/cryptomator.AppImage"
echo "$APP_NAME AppImage downloaded and verified successfully"
# List contents to verify
echo "Contents of $INSTALL_DIR:"
ls -la "$INSTALL_DIR"
EOF_CRYPTO
# Copy and execute the download script in root space (template)
qvm-copy-to-vm "$VAULT_TEMPLATE" "$temp_crypto_script"
qvm-run -p -u root "$VAULT_TEMPLATE" "
mv '/home/user/QubesIncoming/dom0/$(basename "$temp_crypto_script")' /download-cryptomator.sh
chmod +x /download-cryptomator.sh
bash /download-cryptomator.sh
rm -f /download-cryptomator.sh
"
# Clean up
rm -f "$temp_crypto_script"
qvm-run -p "$VAULT_TEMPLATE" "rm -rf '/home/user/QubesIncoming/'" 2>/dev/null || true
# Create desktop entry in persistent location
qvm-run -p -u root "$VAULT_TEMPLATE" "
cat > /usr/share/applications/cryptomator.desktop </dev/null && mv logo.png cryptomator.png || touch cryptomator.png
chmod 644 cryptomator.png
"
echo "Cryptomator GUI installed successfully to persistent location"
}
# Step 3: Create cloud template and install MEGA GUI
create_cloud_template() {
echo -e "\nStep 3: Installing MEGA GUI..."
# Create Cloud Template (MEGAsync)
if ! qvm-check "$CLOUD_TEMPLATE" 2>/dev/null; then
echo "Creating Cloud Template..."
qvm-clone "$BASE_TEMPLATE" "$CLOUD_TEMPLATE"
qvm-prefs "$CLOUD_TEMPLATE" label green
fi
qvm-start --skip-if-running "$CLOUD_TEMPLATE"
# Configure bind-dirs for MEGA
configure_bind_dirs "$CLOUD_TEMPLATE" "mega"
# Set environment
qvm-run -p -u root "$CLOUD_TEMPLATE" "export DEBIAN_FRONTEND=noninteractive"
qvm-run -p -u root "$CLOUD_TEMPLATE" "echo 'TERM=xterm' >> /etc/environment"
qvm-run -p -u root "$CLOUD_TEMPLATE" "sed -i 's/^# *\(en_US.UTF-8\)/\1/' /etc/locale.gen"
qvm-run -p -u root "$CLOUD_TEMPLATE" "locale-gen en_US.UTF-8"
# Install dependencies IN TEMPLATE
qvm-run -p -u root "$CLOUD_TEMPLATE" "
apt-get install -y --no-install-recommends \
qubes-core-agent-passwordless-root \
qubes-core-agent-networking \
curl \
gpg \
firefox-esr \
thunar \
qubes-core-agent-thunar \
mousepad \
less \
psmisc \
xfce4-terminal \
libqt5core5a \
libqt5dbus5 \
libqt5gui5 \
libqt5network5 \
libqt5qml5 \
libqt5quick5 \
libqt5quickwidgets5 \
libqt5svg5 \
libqt5widgets5 \
libqt5x11extras5 \
libc-ares2 \
qml-module-qtquick-dialogs \
qml-module-qtquick-controls \
qml-module-qtquick-controls2 \
libicu72 \
libdouble-conversion3 \
libpcre2-16-0 \
libxcb-xinerama0 \
libssl3
"
# Create mimeapps.list with default MIME type associations
local temp_mime_file=$(mktemp)
cat > "$temp_mime_file" < "$temp_mega_script" <&2
exit 1
fi
# Extract package details for amd64 architecture
echo "Extracting MEGAsync package details..."
PACKAGE_INFO=$(echo "$RESPONSE" | sed -n '/^Package: megasync$/,/^$/p' | grep -A 20 'Architecture: amd64')
if [[ -z "$PACKAGE_INFO" ]]; then
echo "ERROR: Could not find megasync package for amd64 architecture in repository" >&2
exit 1
fi
# Extract download URL for amd64
DOWNLOAD_URL=$(echo "$PACKAGE_INFO" | grep '^Filename:' | sed "s|^Filename: \\./|$MEGA_BASE_URL|" | tr -d '\n' | sed 's/[[:space:]]*$//')
# Show what URL was found
echo "Found URL: $DOWNLOAD_URL"
# If no URL was found, exit with error
if [[ -z "$DOWNLOAD_URL" ]]; then
echo "ERROR: No valid download URL found for $APP_NAME." >&2
echo "Available package info:" >&2
echo "$PACKAGE_INFO" >&2
exit 1
fi
# Extract version for display
VERSION=$(echo "$PACKAGE_INFO" | grep '^Version:' | cut -d' ' -f2)
echo "Found MEGAsync version: $VERSION"
echo "Downloading $APP_NAME from $DOWNLOAD_URL..."
# Download package
if ! curl --tlsv1.2 -x http://127.0.0.1:8082/ -L -o "$TEMP_DIR/megasync.deb" "$DOWNLOAD_URL"; then
echo "ERROR: Download failed." >&2
exit 1
fi
# Hash verification
echo "Verifying integrity..."
# Extract the hash from package info for amd64
EXPECTED_SHA256=$(echo "$PACKAGE_INFO" | grep '^SHA256:' | cut -d' ' -f2)
if [[ -z "$EXPECTED_SHA256" ]]; then
echo "ERROR: Could not extract hash from package info!" >&2
echo "Looking for SHA256 in package info:" >&2
echo "$PACKAGE_INFO" | grep -A 5 -B 5 'SHA256' >&2
rm -f "$TEMP_DIR/megasync.deb"
exit 1
fi
echo "Expected hash: $EXPECTED_SHA256"
ACTUAL_SHA256=$(sha256sum "$TEMP_DIR/megasync.deb" | cut -d' ' -f1)
echo "Found hash: $ACTUAL_SHA256"
if [[ "$EXPECTED_SHA256" != "$ACTUAL_SHA256" ]]; then
echo "ERROR: Hash verification failed!" >&2
echo "Hashes do not match!" >&2
rm -f "$TEMP_DIR/megasync.deb"
exit 1
fi
echo "Hash verification passed"
# Install the package
echo "Installing MEGAsync..."
if dpkg -i "$TEMP_DIR/megasync.deb"; then
echo "MEGAsync installed successfully via dpkg"
elif apt-get install -f -y; then
echo "MEGAsync installed successfully with dependency fix"
else
echo "WARNING: MEGAsync installation had issues but continuing..."
fi
# Check if megasync command is available
if command -v megasync >/dev/null 2>&1 || dpkg -l | grep -q megasync; then
echo 'MEGAsync installation verified successfully'
else
echo 'WARNING: MEGAsync installation may have issues, but continuing setup' >&2
fi
# Clean up
rm -rf "$TEMP_DIR"
echo "$APP_NAME installation process completed"
EOF_MEGA
# Copy and execute the install script in root space (template)
qvm-copy-to-vm "$CLOUD_TEMPLATE" "$temp_mega_script"
qvm-run -p -u root "$CLOUD_TEMPLATE" "
mv '/home/user/QubesIncoming/dom0/$(basename "$temp_mega_script")' /install-megasync.sh
chmod +x /install-megasync.sh
bash /install-megasync.sh
rm -f /install-megasync.sh
"
# Clean up
rm -f "$temp_mega_script"
qvm-run -p "$CLOUD_TEMPLATE" "rm -rf '/home/user/QubesIncoming/'" 2>/dev/null || true
echo "MEGA GUI installed successfully to persistent location"
}
# Step 4: Create AppVMs
create_appvms() {
echo -e "\nStep 4: Creating AppVMs..."
# Create Vault AppVM (air-gapped)
if ! qvm-check "$VAULT_APPVM" 2>/dev/null; then
echo "Creating Vault AppVM..."
qvm-create --template "$VAULT_TEMPLATE" --label black \
--property netvm="" \
--property provides_network=False \
--property vcpus=2 \
--property memory=2048 \
--property maxmem=4096 \
--property autostart=False \
"$VAULT_APPVM"
# Create vault directories in user space (AppVM)
qvm-run -p "$VAULT_APPVM" "
mkdir -p /home/user/Vaults /home/user/Mounts
chown -R user:user /home/user/Vaults /home/user/Mounts
"
# Extend storage
qvm-shutdown --wait "$VAULT_APPVM" 2>/dev/null || true
qvm-volume extend "$VAULT_APPVM:private" "$PRIV_VOL_SIZE" 2>/dev/null || echo "Warning: Could not extend vault volume"
fi
# Create Cloud AppVM (network-enabled)
if ! qvm-check "$CLOUD_APPVM" 2>/dev/null; then
echo "Creating Cloud AppVM..."
qvm-create --template "$CLOUD_TEMPLATE" --label green \
--property netvm="$CLOUD_APPVM_NETVM" \
--property provides_network=False \
--property vcpus=2 \
--property memory=1536 \
--property maxmem=3072 \
--property autostart=False \
"$CLOUD_APPVM"
fi
}
# Step 5: Configure Qubes policies
configure_qubes_policies() {
echo -e "\nStep 5: Configuring Qubes policies..."
local policy_file="/etc/qubes/policy.d/30-gui-cloud-backup.policy"
sudo tee "$policy_file" > /dev/null < "$temp_vault_mgr_script" </dev/null || true
# Cloud Manager script for Cloud AppVM in the root space (template)
local temp_cloud_mgr_script=$(mktemp)
cat > "$temp_cloud_mgr_script" </dev/null)" ]; then
echo "Found backup files in incoming directory:"
ls -la "$INCOMING_DIR"
# Move backups to MEGA sync directory
mkdir -p "/home/user/MEGA/Backups"
find "$INCOMING_DIR" -name "*.tar.gz" -exec mv {} "/home/user/MEGA/Backups/" \;
echo "Backup files moved to MEGA sync directory."
echo "MEGAsync will automatically sync these to your MEGA cloud."
# Clean up
rm -rf "$INCOMING_DIR"
else
echo "No backup files found in incoming directory."
fi
}
open_mega_folder() {
echo "Opening MEGA folder in Thunar..."
thunar "/home/user/MEGA" &
}
setup_mega_sync() {
echo "MEGAsync Setup Instructions:"
echo ""
echo "1. Start MEGAsync from Applications menu"
echo "2. Log in with your MEGA credentials"
echo "3. Add sync pairs:"
echo " - Local: /home/user/MEGA"
echo " - Remote: / (or specific folder in MEGA cloud)"
echo "4. Enable sync for automatic uploads"
echo ""
echo "Backups from Vault AppVM will be automatically synced to MEGA."
}
# Main menu
while true; do
echo ""
echo "Cloud Manager"
echo "1. Process Backups"
echo "2. Open MEGA Folder"
echo "3. MEGA Setup Help"
echo "4. Show Help"
echo "5. Exit"
echo ""
read -p "Choose an option (1-5): " CHOICE
case "$CHOICE" in
1)
process_backups
;;
2)
open_mega_folder
;;
3)
setup_mega_sync
;;
4)
show_help
;;
5|"")
break
;;
*)
echo "Invalid option"
;;
esac
done
EOF_CLOUD_MGR
qvm-copy-to-vm "$CLOUD_TEMPLATE" "$temp_cloud_mgr_script"
qvm-run -p -u root "$CLOUD_TEMPLATE" "
mkdir -p /opt
mv '/home/user/QubesIncoming/dom0/$(basename "$temp_cloud_mgr_script")' /opt/cloud-manager
chmod +x /opt/cloud-manager
chown user:user /opt/cloud-manager
"
# Clean up
rm -f "$temp_cloud_mgr_script"
qvm-run -p "$CLOUD_TEMPLATE" "rm -rf '/home/user/QubesIncoming/'" 2>/dev/null || true
echo "Integration scripts installed to persistent locations"
}
# Step 7: Create desktop entries
create_desktop_entries() {
echo -e "\nStep 7: Creating desktop entries..."
# Vault Manager desktop entry in the root space (template)
local temp_vault_desktop=$(mktemp)
cat > "$temp_vault_desktop" </dev/null || true
# Cloud Manager desktop entry
local temp_cloud_desktop=$(mktemp)
cat > "$temp_cloud_desktop" </dev/null || true
echo "Desktop entries created in persistent locations"
}
# Step 8: Final setup
finalize_setup() {
echo -e "\nStep 8: Finalizing setup..."
# Sync appmenus and shutdown
qvm-start --skip-if-running "$VAULT_TEMPLATE"
qvm-sync-appmenus "$VAULT_TEMPLATE"
qvm-shutdown --wait "$VAULT_TEMPLATE"
qvm-start --skip-if-running "$CLOUD_TEMPLATE"
qvm-sync-appmenus "$CLOUD_TEMPLATE"
qvm-shutdown --wait "$CLOUD_TEMPLATE"
qvm-start --skip-if-running "$VAULT_APPVM"
qvm-sync-appmenus "$VAULT_APPVM"
qvm-shutdown --wait "$VAULT_APPVM"
qvm-start --skip-if-running "$CLOUD_APPVM"
qvm-sync-appmenus "$CLOUD_APPVM"
qvm-shutdown --wait "$CLOUD_APPVM"
# Configure menu items using desktop entries from persistent locations (template)
qvm-features "$VAULT_TEMPLATE" menu-items "xfce4-terminal.desktop thunar.desktop org.xfce.mousepad.desktop"
qvm-features "$CLOUD_TEMPLATE" menu-items "xfce4-terminal.desktop thunar.desktop org.xfce.mousepad.desktop"
qvm-features "$VAULT_APPVM" menu-items "cryptomator.desktop vault-manager.desktop thunar.desktop org.xfce.mousepad.desktop xfce4-terminal.desktop"
qvm-features "$CLOUD_APPVM" menu-items "cloud-manager.desktop firefox-esr.desktop megasync.desktop thunar.desktop org.xfce.mousepad.desktop xfce4-terminal.desktop"
}
# Main installation function
main() {
echo "Starting Cloud Backup Setup..."
create_base_template
create_vault_template
download_cryptomator
create_cloud_template
download_install_megasync
create_appvms
configure_qubes_policies
create_integration_scripts
create_desktop_entries
finalize_setup
echo -e "\nSetup completed successfully!"
}
# Run main function
main "$@"