Centralised file backup qube (Cryptomator)

I'm also thinking about this topic "centralised file backup qube" and trying to figure out a user-friendly setup.

Workflow concept for secure cloud backup:

Source AppVMs → Vault AppVM → Cloud AppVM → MEGA Cloud
     ↓              ↓              ↓              ↓
  [Files]     [Encryption and  [Network    [Cloud Storage]
                Air-Gapped]      Sync]

Setup features:

  • 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 "$@"

Complex scripts dont render correctly, so:

qubes-gui-cloud-backup.sh.log (25.7 KB)



References

Core References

Qubes OS Backup Setups

Qubes OS Forum

Qubes OS Documentation


RCLONE References

MEGA References

gocryptfs References

Cryptomator References

Other references