USB Kill Switch for Qubes OS - Physical Security Enhancement

This guide explains how to set up a USB-based kill switch for Qubes OS that locks your screen immediately and shuts down your system shortly after a specific USB device is removed.
It’s similar to BusKill, but purpose-built for Qubes OS.
ONLY TESTED FOR XFCE ENVIRONMENT WITH THE XFCE SCREENSAVER

What it does:

  • Monitors a chosen USB device (flash drive, hardware token, etc.)
  • Locks screen immediately if device is disconnected
  • Shuts down after a set delay if screen remains locked
  • Resumes monitoring if you unlock before shutdown

Prerequisites

  • Qubes OS (tested on 4.2.4)
  • A USB device to act as your “key”
  • Basic terminal knowledge

Installation

1. Create Script Directory

mkdir -p ~/Scripts
cd ~/Scripts

2. Create the Scripts

SelectUSB.sh – Choose USB device to monitor

#!/bin/bash
ConfigDir="$HOME/Scripts"
mkdir -p "$ConfigDir"

ListDevices() {
    echo "===== Available USB Block Devices ====="
    Devices=()
    Types=()
    Index=1
    while IFS= read -r Line; do
        BackendDevice=$(echo "$Line" | awk '{print $1}')
        Description=$(echo "$Line" | cut -d' ' -f3-)
        Devices+=("$BackendDevice")
        Types+=("block")
        echo "$Index) $BackendDevice  -  $Description"
        ((Index++))
    done < <(qvm-block list 2>/dev/null | tail -n +2)

    echo
    echo "===== Available Raw USB Devices ====="
    while IFS= read -r Line; do
        BackendDevice=$(echo "$Line" | awk '{print $1}')
        Description=$(echo "$Line" | cut -d' ' -f3-)
        Devices+=("$BackendDevice")
        Types+=("usb")
        echo "$Index) $BackendDevice  -  $Description"
        ((Index++))
    done < <(qvm-usb list 2>/dev/null | tail -n +2)
}

ChooseDevice() {
    while true; do
        clear
        echo "USB Kill Switch - Device Selection"
        echo "=================================="
        ListDevices
        echo
        echo "Enter number to select device, 'r' to refresh, or 'q' to quit:"
        read -rp "> " Choice
        case "$Choice" in
            "r"|"R") continue ;;
            "q"|"Q") exit 0 ;;
            *)
                if [[ "$Choice" =~ ^[0-9]+$ ]] && (( Choice >= 1 && Choice <= ${#Devices[@]} )); then
                    SelectedDevice="${Devices[$((Choice-1))]}"
                    MonitorType="${Types[$((Choice-1))]}"
                    MonitorVM=$(echo "$SelectedDevice" | cut -d':' -f1)
                    MonitorDevice=$(echo "$SelectedDevice" | cut -d':' -f2)
                    echo
                    echo "Selected: $MonitorDevice from $MonitorVM (type: $MonitorType)"
                    echo "Press Enter to confirm..."
                    read
                    break
                else
                    echo "Invalid choice! Try again."
                    sleep 2
                fi
                ;;
        esac
    done
}

echo "Starting USB Kill Switch setup..."
ChooseDevice
echo "MonitorType=$MonitorType" > "$ConfigDir/USBDevice.conf"
echo "MonitorVM=$MonitorVM" >> "$ConfigDir/USBDevice.conf"
echo "MonitorDevice=$MonitorDevice" >> "$ConfigDir/USBDevice.conf"

echo "Configuration saved. Starting USB Kill Switch service..."
systemctl --user start monitor-drive-usbkill.service
echo "USB Kill Switch is now active!"
sleep 3

Tip:
If you want this selector to appear automatically after login (e.g., so you can choose your USB key every session), create a .desktop file in ~/.config/autostart:

[Desktop Entry]
Type=Application
Name=USB Kill Switch Setup
Exec=xfce4-terminal --command "/home/user/Scripts/SelectUSB.sh"
Terminal=true
X-GNOME-Autostart-enabled=true

Replace /home/user with your actual username.


USBKill.sh – Main monitoring daemon

#!/bin/bash
ConfigDir="$HOME/Scripts"
ConfigFile="$ConfigDir/USBDevice.conf"

if [[ ! -f "$ConfigFile" ]]; then
    echo "ERROR: No USB device configured."
    echo "Please run SelectUSB.sh first."
    exit 1
fi

source "$ConfigFile"

ShutdownTimeout=10
CheckInterval=0.5
DisconnectedTime=0

LogMessage() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | systemd-cat -t usbkill
}

IsDeviceConnected() {
    if [[ "$MonitorType" == "block" ]]; then
        qvm-block list "$MonitorVM" 2>/dev/null | grep -q "$MonitorDevice"
    else
        qvm-usb list "$MonitorVM" 2>/dev/null | grep -q "$MonitorDevice"
    fi
}

IsScreenLocked() {
    xscreensaver-command -time 2>/dev/null | grep -q "screen locked"
}

LockScreen() {
    if ! IsScreenLocked; then
        LogMessage "Locking screen due to USB disconnect"
        xflock4 &
    fi
}

LogMessage "USB Kill Switch started - Monitoring $MonitorType device $MonitorDevice on $MonitorVM"

while true; do
    if [[ $DisconnectedTime -eq 0 ]]; then
        if ! IsDeviceConnected; then
            DisconnectedTime=$(date +%s)
            LogMessage "USB device disconnected"
            LockScreen
        fi
    else
        CurrentTime=$(date +%s)
        ElapsedTime=$((CurrentTime - DisconnectedTime))

        if ! IsScreenLocked; then
            LogMessage "Screen unlocked — stopping service"
            systemctl --user stop monitor-drive-usb.service
            xfce4-terminal -x "$ConfigDir/SelectUSB.sh"
            exit 0
        fi

        if [[ $ElapsedTime -ge $ShutdownTimeout ]]; then
            LogMessage "Timeout reached — shutting down"
            shutdown now
            exit 0
        fi
    fi
    sleep $CheckInterval
done

3. Set Permissions

chmod +x ~/Scripts/*.sh

4. Create Systemd Service

File: ~/.config/systemd/user/monitor-drive-usbkill.service

[Unit]
Description=USB Kill Switch Monitor
After=graphical-session.target
Wants=graphical-session.target

[Service]
Type=simple
ExecStart=%h/Scripts/USBKill.sh
Restart=on-failure
RestartSec=10
Environment=DISPLAY=:0

[Install]
WantedBy=default.target

Usage

  1. Run:
~/Scripts/SelectUSB.sh
  1. Pick your USB key
  2. Service runs automatically and monitors the device

Customization

  • ShutdownTimeout=30 – seconds before shutdown
  • CheckInterval=0.5 – polling interval in seconds

Troubleshooting

  • Check logs:
journalctl --user -u monitor-drive-usbkill.service --no-pager
  • Make sure xscreensaver is running for screen lock

Uninstall

systemctl --user stop monitor-drive-usbkill.service
rm -rf ~/Scripts/
rm ~/.config/systemd/user/monitor-drive-usbkill.service
systemctl --user daemon-reload
10 Likes

Buskill has a Qubes specific guide

I never presume to speak for the Qubes team.
When I comment in the Forum I speak for myself.

4 Likes

I know BusKill’s Qubes guide. This is for people who don’t have BusKill. It works with any external device (USB, SD, external SSD) and is built specifically for Qubes OS.

3 Likes

@juicewrld thanks for sharing :slight_smile:

To be clear: BusKill was originally designed specifically for QubesOS. When I created BusKill in 2017, QubesOS was my main OS (and it still is today).

And BusKill also works with any USB device. We do sell an open-hardware BusKill cable on our website, but that’s just to lower the barrier of entry and make it more accessible to non-techie folks (eg many journalists, whistleblowers, human rights workers, etc).

Anyone can make their own BusKill cable.

The cross-platform software and our 2-part QubesOS guide will work with any USB device.

For more information, please see our documentation:

Please feel free to submit a feature request, if you think that BusKill could be improved for Qubes.

2 Likes

A few months ago, I found a system that does almost the same thing but with a different approach and I use a usb key specially dedicated to that (an old 64MB usb key).

The method is different in the sense that I create a rule in sys-usb then, in dom0 a .policy rule and a small script that allows to define the action to execute in case of removal of the key.
It was inspired by an article @solene : Solene'% : How to trigger a command on a running Linux laptop when disconnected from power

I take advantage of this topic (I had not seen it before! ) to post this approach.

  1. In sys-usb:
  • Create a rule in /etc/udev/rules.d/ in sys-usb:

    sudo vim /etc/udev/rules.d/99-usb-remove.rules

    Add that in it :

    ACTION=="remove", SUBSYSTEM=="block", ENV{ID_SERIAL}=="Numero_de_série", RUN+="/usr/bin/qrexec-client-vm dom0 qubes.USBRemoved"

    To find the serial number, identify the device with fdisk -l (in this example: /dev/sdX) and type the command:

    udevadm info --query=all --name=/dev/sdX

    Search the section: ID_SERIAL to find the serial number.

  • recharge the rules with :
    sudo udevadm control --reload-rules

  1. In Dom0 :
  • Create a script to define the action :

    sudo vim /usr/local/bin/usb_removed_action.sh
    sudo chmod +x /usr/local/bin/usb_removed_action.sh

    For example to suspend to system :

    #!/bin/bash
    systemctl suspend
    
  • Create a file in the directory /etc/qubes/policy.d :

    sudo vim /etc/qubes/policy.d/50-usb-remover.policy

    with the content:

    qubes.USBRemoved * sys-usb dom0 allow

Now, by disconnecting the key, the command of the /usr/local/bin/usb_removed_action.sh file will be activated.

6 Likes

Great info, thank you!

2 Likes

having a cable attached to your body etc… is a bit of giveaway that your doing something nefarious. I would rather have a swob in my pocket (like for your garage door) where I could push a button and have the same effect, or when I am out of radius it would do that automatically.

1 Like

Yeah, the cable thing is kind of ironic. the whole point is to be discreet and then you’ve got a wire attached to your wrist.

But there are basically two completely different problems here

Just a button = super easy

If you don’t need proximity and just want to hit a button when things go sideways, a cheap 433MHz remote from AliExpress is literally like $3 and works out of the box. You get a little fob that looks like a garage door opener (which is exactly what you asked for lol), wire a receiver to a Pi Pico or ESP32, and write maybe 20 lines of code to catch the signal and call your kill script. Battery lasts years. Done.

One thing though, if your threat model is serious enough that you’re running Qubes, be aware that the really cheap 433MHz remotes usually use fixed codes. Anyone with a cheap RTL-SDR can sniff the signal once and replay it later. So either use a rolling-code 433MHz remote (KeeLoq, same price range) or just use Bluetooth, which handles this stuff properly already.

For Bluetooth, the cleanest option IMO is one of those cheap BLE music remotes people use for selfies or Spotify controls, like $5 on AliExpress. Pair it to a Pi Pico W or ESP32 and you’re basically done. The remote shows up as a keyboard/HID device, so you just listen for button events and trigger your script. The Pico W and ESP32 can also act as HID devices themselves if you want the microcontroller injecting keystrokes over USB, which is honestly a really clean setup.

Proximity (auto-trigger when you walk away) = genuinely annoying

This is where it gets messy. RSSI/signal strength is not distance. It fluctuates constantly depending on your body position, walls, reflections, even how you’re holding the remote.

So you need debouncing:

  • don’t trigger on one bad reading
  • trigger after 2–3 consecutive weak/missing readings
  • with a 3–5 second poll interval that’s usually a 6–15 second reaction time

For actual ranging:

  • BLE RSSI → most practical. ESP32 or Pico W scans for the remote, averages readings, triggers after several weak checks. Same cheap BLE remote can handle both button + proximity.
  • UWB → if you want it to actually be accurate. DW3000/Qorvo modules are ~$20–30 each, need one on both ends, but you get centimeter-level ranging and way fewer false triggers. Basically the “real solution.”

Don’t try to do proximity with 433MHz btw. It’s great for buttons, terrible for ranging. The signal goes through everything and you genuinely can’t tell 3 meters from 30 meters reliably.

Qubes-specific thing nobody mentions

You probably don’t want this daemon running in dom0. Better setup is a dedicated qube that owns the Bluetooth adapter, handles detection, and talks to dom0 through a super limited qrexec policy that can only trigger the one lock/kill command. Keeps dom0 isolated and limits damage if the detection qube ever gets compromised.

TL;DR:

  • no proximity → 433MHz rolling-code fob + ESP32/Pico W
  • want auto-lock → BLE RSSI with debouncing
  • want it actually reliable → UWB
  • never use 433MHz for ranging
1 Like