Skip to content

Instantly share code, notes, and snippets.

@pietrushnic
Created September 23, 2025 12:15
Show Gist options
  • Save pietrushnic/53efd2923a87ee0c738f45958ead6c15 to your computer and use it in GitHub Desktop.
Save pietrushnic/53efd2923a87ee0c738f45958ead6c15 to your computer and use it in GitHub Desktop.
RemoteVM PoC Guide (Qubes Air)

RemoteVM PoC Guide (Qubes Air)

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, and remote_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

Prerequisites

  • 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-vm

Naming 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

Part 1 — Single-Host PoC (relay-only)

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-remote

Set 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.Relay

Install 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: test

Troubleshooting:

  • EOF 0/1 KB: ensure /usr/local/etc/qubes-rpc/test.Relay is executable
  • “target 'vm2-remote_qubes.Filecopy' … denied”: confirm the script trims -remote and the test.Relay policy exists
  • Logs: journalctl -u qubes-policy-daemon -b (dom0)

Reference: integration test qubes/tests/integ/misc.py, class TC_10_RemoteVMMixin.


Part 2 — Mixed 4.3 ↔ 4.2 via SSH (ProxyJump)

Topology:

  • Local-Relay (4.3): runs qubesair.SSHProxy RPC
  • 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 true

Local-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 3

Use 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-qube for 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: vm2

Policies:

  • 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: test

Notes:

  • On 4.2, the source will appear as remote-relay (no --source-qube support)
  • If ssh remote-relay-through-bastion true fails from relay, fix keys, resolve bastion DNS, and verify the reverse tunnel is active

Debugging Cheat Sheet

  • 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 true

Common issues:

  • EOF 0/1 KB: missing exec bit on RPC script; or wrong path (/usr/local/etc/qubes-rpc overrides /etc/qubes-rpc)
  • target … does not exist: forgot to strip -remote in 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; sshd not running in remote-relay

Cleanup

  • Remove /etc/qubes/policy.d/30-remotevm-poc.policy on 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

References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment