Automatically attaching known devices by UUID (like SD cards)

This is how to automatically attach USB devices that have a ID_FS_UUID property to your assigned VM.

The scripts below are based on Padhi’s scripts for attaching USB devices but hardened. Meaning:

  • It will attach only devices that have been whitelisted.
  • It will execute dom0 scripts qrexec-client-vm without parameters, decreasing the attack surface.
  • It will execute only whitelisted dom0 scripts.

In the template

Install python3-systemd for accessing the logs with journalctl.

sudo apt install python3-systemd

In sys-usb (or sys-usb-dvm)

sudo mkdir /rw/config/attach-known-device

/rw/config/attach-known-device/attach_known_device.py:

#!/usr/bin/env python3

import logging
import os
import pyudev
import subprocess
from systemd import journal
import sys


FORMAT = '[%(filename)s:%(lineno)s - %(funcName)20s() ] %(message)s'
logging.basicConfig(format=FORMAT)
logging.getLogger().setLevel(logging.DEBUG)
logging.StreamHandler(sys.stderr).setLevel(logging.DEBUG)
logging.debug('Level DEBUG set')


# whitelist of devices
UUID_TO_DEVICE_NICKNAME = {
    # YOU NEED TO EDIT THE LINE BELOW!!!
    "345b0046-5c19-1111-abcd-abe331a4a127": "backups-microsd",
}

# whitelist of dom0 qrexec scripts
DOM0_QREXEC_SCRIPTS = {
    # Add and remove your own scripts for your devices
    "backups-microsd-at-sdb": "custom.attach-backups-microsd",
}


def log_msg(message):
    # Print on console if called from CLI, and on systemd journal otherwise
    if sys.stdout.isatty():
        logging.info(message)
    else:
        journal.send(message, SYSLOG_IDENTIFIER="attach-known-device")


def get_uuid_from_device(device_path):
    full_device_path = f"/dev/{device_path}"
    context = pyudev.Context()
    uuid = None

    try:
        device = pyudev.Devices.from_device_file(context, full_device_path)
    except pyudev.DeviceNotFoundByFileError:
        log_msg(f"ERROR: Device file {full_device_path} not found.")
        return

    uuid = device.properties.get('ID_FS_UUID')
    if uuid is None:
        err_msg = f"ID_FS_UUID property not found in device properties."
        log_msg(f"ERROR: {err_msg}")
        raise ValueError(err_msg)
    else:
        return uuid


def attach_device_to_corresponding_vm(device_nickname, device_path):
    dom0_qrexec_script = DOM0_QREXEC_SCRIPTS.get(
            f"{device_nickname}-at-{device_path}"
    )

    if dom0_qrexec_script is None:
        err_msg = (f"dom0_qrexec_script not whitelisted: "
                   "{dom0_qrexec_script}")
        log_msg(f"ERROR: {err_msg}")
        raise ValueError(err_msg)
    
    log_msg(f"DEBUG: dom0_qrexec_script = {dom0_qrexec_script}")
    args = ["qrexec-client-vm", "dom0", dom0_qrexec_script,]
    log_msg(f"Executing {args}")
    try:
        process = subprocess.Popen(args, stdout=subprocess.PIPE,
                stderr=subprocess.PIPE)
        stdout, stderr = process.communicate()
        if stdout:
            log_msg("Output:")
            log_msg(f"{stdout.decode('utf-8')}")
        if stderr:
            log_msg(f"ERROR: {stderr.decode('utf-8')}")
    except subprocess.CalledProcessError:
        log_msg(f"ERROR: Failed to attach device {device_nickname} at "
                "{device_path}.")
        raise


if __name__ == "__main__":
    log_msg("Executing attach-known-device.py")
    if len(sys.argv) != 2:
        log_msg("Usage: attach-known-device <device_path>"
                " (e.g. sda1, not /dev/sda1)")
        sys.exit(1)

    device_path = sys.argv[1]
    log_msg(f"Device path: {device_path}")
    uuid = get_uuid_from_device(device_path)
    if not uuid:
        log_msg("ERROR: UUID not found.")
        exit(1)
    else:
        log_msg(f"UUID: {uuid}")

    device_nickname = UUID_TO_DEVICE_NICKNAME.get(uuid)
    if device_nickname is None:
        log_msg(f"INFO: unknown device: {uuid}")
        exit(0)
    try:
        attach_device_to_corresponding_vm(device_nickname, device_path)
    except subprocess.CalledProcessError:
        exit(1)

/rw/config/attach-known-device/90-attach-known-device.rules:

# From https://saswat.padhi.me/blog/2021-09_usb-autoattach-in-qubes/#fabusbfa-changes-in-sysusb
ACTION=="add", SUBSYSTEM=="block", KERNEL=="sd[a-z]", \
        RUN+="/bin/sh -c '/rw/config/attach-known-device/attach_known_device.py %k &'"

And add this to /rw/config/rc.local:

ln -sv /rw/config/attach-known-device/90-attach-known-device.rules \
        /etc/udev/rules.d/

udevadm control --reload && udevadm trigger 2>&1

In dom0

These are for my use case, but you can modify them and rename them.

/etc/qubes-rpc/custom.attach-backups-microsd:

#!/usr/bin/env bash

# Modified from https://saswat.padhi.me/blog/

# Message is displayed in sys-usb, that is calling this script with qrexec-client-vm
echo "/etc/qubes-rpc/custom.attach-microsd-backups-as-sdb.sh"
LOG="systemd-cat --identifier=attach-known-device"
echo "/etc/qubes-rpc/custom.attach-microsd-backups-as-sdb.sh" | $LOG

# Exit immediately if any of the following commands fail
set -e

#XXX race condition?
sleep 1

# Start the target VM, if needed
echo "qvm-start --skip-if-running backups" | $LOG
qvm-start --skip-if-running backups

# Attach the target device from remote VM to target VM
echo "qvm-block attach backups sys-usb:sdb" | $LOG
qvm-block attach backups sys-usb:sdb


echo "Starting qubes-backup" | $LOG
DISPLAY=:0.0 qubes-backup

{
	echo "10"
	sleep 1
	echo "50"
	sleep 1
	echo "100"
} | DISPLAY=:0.0 zenity --progress --auto-close --no-cancel --text="split-bak"
if DISPLAY=:0.0 zenity --question --text="Run split-bak.sh in backups VM?"; then
	qvm-run --pass-io backups -- /usr/local/bin/split-bak.sh
else
	exit 0
fi

{
	echo "10"
	sleep 1
	echo "50"
	sleep 1
	echo "100"
} | DISPLAY=:0.0 zenity --progress --auto-close --no-cancel --text="upload-bak"
if DISPLAY=:0.0 zenity --question --text="Run upload-bak.sh on backups?"; then
	qvm-run --pass-io backups -- /usr/local/bin/upload-bak.sh
else
	exit 0
fi

# Message is displayed in sys-usb, that is calling this script with qrexec-client-vm
echo "End of /etc/qubes-rpc/custom.attach-microsd-backups-as-sdb.sh"
# One more time for journalctl
echo "End of /etc/qubes-rpc/custom.attach-microsd-backups-as-sdb.sh" | $LOG

/etc/qubes-rpc/policy/custom.attach-backups-microsd:

## From https://saswat.padhi.me/blog/ 
sys-usb	dom0	ask,default_target=dom0

@anyvm	@anyvm	deny
2 Likes

Thank you for posting this!

Is there also a way to blacklist any usb devices from attaching to a VM?
I’m a nightmare in the early morning.
Still half asleep, I’ll connect my ethernet usb dongle to my Vault…

Qubes OS 4.3 will ship with an integrated device manager allowing exactly this :slight_smile:

2 Likes

Yes, although @gasull 's solution will still be useful until Device-identity-based assignments do not work correctly with LUKS/dm devices · Issue #10147 · QubesOS/qubes-issues · GitHub is fixed.

2 Likes