Version: v2025.09.23
This guide reproduces two RemoteVM demonstrations:
- Single-host PoC (relay-only) that mirrors the TC_10_RemoteVMMixin integration test
- Mixed 4.3 ↔ 4.2 PoC using SSH (ProxyJump) without changing Qubes firewall rules
Outcomes:
- Understand and exercise
relayvm,transport_rpc, andremote_name - Copy a file from a local qube to a RemoteVM, including a cross-host hop
- Know where to put the relay scripts and how to write policies
Notes:
- PoC only; review security before production use
- Qubes 4.2 does not support
--source-qube; source attribution will appear as the relay qube on the 4.2 side
- Local host: Qubes OS 4.3 (rc2+), with debian-13-minimal template available
- Remote host: Qubes OS 4.2
- Optional bastion host (or router) reachable from both sides (used only as SSH jump; no Qubes firewall changes required)
Template prep (Debian 13 minimal):
# In dom0, clone the template for experimentation
qvm-clone debian-13-minimal deb13-remote-base
# In the template (root shell)
qvm-run -u root deb13-remote-base xterm
# Inside the template:
apt update && apt install -y openssh-client
# Optional:
apt install -y qubes-core-agent-passwordless-root
# Verify qrexec client exists:
ls /usr/lib/qubes/qrexec-client-vmNaming used throughout the guide (you may adapt names as needed):
- On 4.3:
relay(Local-Relay),vm1(source),vm2(local target),vm1-remote,vm2-remote(RemoteVM objects) - On 4.2:
remote-relay(Remote-Relay),vm2(remote target) - Bastion:
ollama.local(SSH reachable) - SSH alias in Local-Relay:
remote-relay-through-bastion
This reproduces the core of integration test TC_10_RemoteVMMixin.
Create qubes on 4.3:
qvm-create -t deb13-remote-base -l green relay
qvm-create -t deb13-remote-base -l red vm1
qvm-create -t deb13-remote-base -l red vm2
qvm-create -C RemoteVM -l red vm1-remote
qvm-create -C RemoteVM -l red vm2-remoteSet RemoteVM properties (4.3 dom0):
qvm-prefs vm1-remote relayvm relay
qvm-prefs vm1-remote transport_rpc test.Relay
qvm-prefs vm2-remote relayvm relay
qvm-prefs vm2-remote transport_rpc test.RelayInstall transport RPC in relay (qube-local path preferred):
- Path:
/usr/local/etc/qubes-rpc/test.Relay - Make it executable
- Content:
#!/bin/sh
set -eu
in="${1:-}"
target="${arg%%+*}"
# map vmX-remote -> vmX locally
target="${target%-remote}"
service="${arg#*+}"
exec qrexec-client-vm --source-qube="${QREXEC_REMOTE_DOMAIN}-remote" -- "$target" "$service"Policy (4.3 dom0), file /etc/qubes/policy.d/30-remotevm-poc.policy:
# Allow the transport RPC from vm1 to relay
test.Relay * vm1 relay allow
# First hop: vm1 -> vm2-remote
qubes.Filecopy * vm1 vm2-remote allow
# Second hop: vm1-remote -> vm2
qubes.Filecopy * vm1-remote vm2 allow
Test:
# In vm1
printf test > ~/test-file.txt
qvm-copy-to-vm vm2-remote ~/test-file.txt
# In vm2
cat ~/QubesIncoming/vm1-remote/test-file.txt # expect: testTroubleshooting:
- EOF 0/1 KB: ensure
/usr/local/etc/qubes-rpc/test.Relayis executable - “target 'vm2-remote_qubes.Filecopy' … denied”: confirm the script trims
-remoteand thetest.Relaypolicy exists - Logs:
journalctl -u qubes-policy-daemon -b(dom0)
Reference: integration test qubes/tests/integ/misc.py, class
TC_10_RemoteVMMixin.
Topology:
- Local-Relay (4.3): runs
qubesair.SSHProxyRPC - Remote-Relay (4.2): reachable via SSH through bastion
- Bastion host: jump point; no Qubes firewall changes required
Remote-Relay (4.2): install SSH and create a reverse tunnel to bastion
# In remote-relay (Debian example)
sudo apt update && sudo apt install -y openssh-server
sudo systemctl enable --now ssh
# From remote-relay, run reverse tunnel to the bastion
ssh -N -R 2022:localhost:22 [email protected]
# On the bastion, sanity-check it reaches remote-relay
ssh -p 2022 user@localhost trueLocal-Relay SSH config (4.3): ~/.ssh/config
Host bastion
HostName bastion.local
User bastion-user
Host remote-relay-through-bastion
HostName localhost
Port 2022
User user
ProxyJump bastion
ServerAliveInterval 30
ServerAliveCountMax 3Use key-based auth for both the bastion and remote-relay to avoid password
prompts. Install the corresponding public keys into ~/.ssh/authorized_keys
for bastion-user on bastion and user on remote-relay.
Install SSH transport RPC in relay (4.3):
- Path:
/usr/local/etc/qubes-rpc/qubesair.SSHProxy - Make it executable
- Content (no
--source-qubefor 4.2 compatibility):
#!/bin/sh
set -eu
in="${1:-}"
IFS='+' read -r target service <<EOF
$in
EOF
# Map local RemoteVM name to actual remote name
remote_qube="$(qubesdb-read /remote/$target 2>/dev/null || echo $target)"
# Forward to remote-relay via bastion tunnel
exec ssh remote-relay-through-bastion qrexec-client-vm "$remote_qube" "$service"Define RemoteVM on 4.3 and map names:
qvm-prefs vm2-remote relayvm relay
qvm-prefs vm2-remote transport_rpc qubesair.SSHProxy
qvm-prefs vm2-remote remote_name vm2
# Start relay once so mapping is written
qvm-start relay || true
# In relay, verify mapping
qubesdb-read /remote/vm2-remote # expect: vm2Policies:
- 4.3 dom0
/etc/qubes/policy.d/30-remotevm-poc.policy(add):
qubesair.SSHProxy * vm1 relay allow
qubes.Filecopy * vm1 vm2-remote allow
- 4.2 dom0
/etc/qubes/policy.d/30-remotevm-poc.policy:
qubes.Filecopy * remote-relay vm2 allow
Test end-to-end:
# In vm1 (4.3)
printf test > ~/test.txt
qvm-copy-to-vm vm2-remote ~/test.txt
# In vm2 (4.2)
cat ~/QubesIncoming/remote-relay/test.txt # expect: testNotes:
- On 4.2, the source will appear as
remote-relay(no--source-qubesupport) - If
ssh remote-relay-through-bastion truefails fromrelay, fix keys, resolve bastion DNS, and verify the reverse tunnel is active
- Policy engine logs (dom0):
journalctl -u qubes-policy-daemon -b- QubesDB mapping in relay:
qubesdb-list /remote/
qubesdb-read /remote/<RemoteVMName>- Manual policy probes (from relay):
# This must be allowed by 4.2 policy
qrexec-client-vm vm2 qubes.Filecopy </dev/null- SSH connectivity (from relay):
ssh -vvv remote-relay-through-bastion trueCommon issues:
- EOF 0/1 KB: missing exec bit on RPC script; or wrong path
(
/usr/local/etc/qubes-rpcoverrides/etc/qubes-rpc) - target … does not exist: forgot to strip
-remotein the local relay path; or mis-typed service argument - Request refused: missing one of the policy lines on 4.3 or 4.2
- Connection refused: inactive reverse tunnel; SSH ProxyJump misconfigured;
sshdnot running inremote-relay
- Remove
/etc/qubes/policy.d/30-remotevm-poc.policyon both hosts if created only for the demo - Remove relay scripts from
/usr/local/etc/qubes-rpc/if not needed - Stop the reverse tunnel and clean up SSH keys as desired
- Integration test reference:
qubes/tests/integ/misc.py,TC_10_RemoteVMMixin - RemoteVM class/properties:
qubes/vm/remotevm.py(relayvm,transport_rpc,remote_name) - Relay extension and QubesDB mapping:
qubes/ext/relay.py(writes/remote/<RemoteVM>in the relay’s QubesDB) - RemoteVM docs: https://dev.qubes-os.org/projects/qubes-core-qrexec/en/latest/qrexec-remotevm.html