Step-by-step Guide on setting up an OpenVPN Gateway with multiple anti-leak features that make the connection fail closed should it be interrupted.
It has been tested with Debian 13 minimal on Qubes versions 4.2 and 4.3.
Security and privacy features included: - đ anti-leak nftables rules applied to the VPN Gateway VM from inside the VM - đ anti-leak dom0 rules applied to the VPN Gateway VM from outside - :male_detective:optional random OpenVPN connection selection (requires multiple .ovpn files) - :male_detective:optional no-logs OpenVPN client configuration
Before proceeding, you will need to download a copy of your VPN provider's configuration file(s) and have your VPN login information handy.
In dom0 terminal, install debian-13-minimal template
qvm-template install debian-13-minimal
Clone the template to a dedicated OpenVPN template
qvm-clone debian-13-minimal debian-13-minimal-ovpn
Start XTerm terminal in the OpenVPN template
qvm-run -u root debian-13-minimal-ovpn xterm
In debian-13-minimal-ovpn terminal, edit XTerm terminal settings to allow copy-pasting
nano /etc/X11/app-defaults/XTerm
scroll to the end, paste the following line
*selectToClipboard: true
Save the file (ctrl+x, y, and Enter)
Close the terminal, then open it again (repeat step #3)
<small> âšī¸ </small>Copy-Paste inside XTerm Terminal: to copy, select the text and press
Ctrl+Shift+C, to paste press the mousewheel button
Update and install OpenVPN and notification packages
apt update && apt install -y --no-install-recommends qubes-core-agent-networking qubes-notification-agent libnotify-bin openvpn
<small>Note: libnotify-bin package is present in debian-13 template, but missing in the minimal version. It is required for VPN status notifications.</small>
systemctl stop openvpn.service
systemctl stop openvpn-client@client
systemctl stop openvpn-server@server
systemctl disable openvpn.service
systemctl disable openvpn-client@client
systemctl disable openvpn-server@server
qvm-shutdown debian-13-minimal-ovpnsys-vpndebian-13-minimal-ovpnCustom â sys-firewall â <small> â ī¸ </small> not Default â sys-firewallProvides network access to other qubes checked in the Advanced Options sectionMake sure the VPN template debian-13-minimal-ovpn is not running, then start the VPN VM and launch XTerm terminal. From dom0 run
qvm-run -u root sys-vpn xterm
Set up and test the VPN client. Create a new `/rw/config/vpn` folder
mkdir /rw/config/vpn
Copy your VPN configuration files to `/rw/config/vpn`. Your OpenVPN config file should be named `openvpn-client.ovpn` so you can use the scripts below as is without modification. Otherwise you would have to replace the file name. Files accompanying the main config such as `*.crt` and `*.pem` should also be placed in the `/rw/config/vpn` folder.
Check or modify configuration file contents
nano /rw/config/vpn/openvpn-client.ovpn
Files referenced in `openvpn-client.ovpn` should not use absolute paths such as `/etc/...`
The config should route all traffic through your VPN's interface after a connection is created; For OpenVPN the directive for this is `redirect-gateway def1`
Make sure it already includes or add:
`redirect-gateway def1`
> **Note:** If your VPN provider did not supply credentials to use, you may skip the following `auth-user-pass` directive and `pass.txt` file creation
The VPN client may not be able to prompt you for credentials when connecting to the server, so we'll add a reference to a file containing the VPN username and password, add or modify `auth-user-pass` like so:
`auth-user-pass pass.txt`
Save the `/rw/config/vpn/openvpn-client.ovpn` file.
`nano /rw/config/vpn/pass.txt`
Add:
```text
username
password
```
Replace `username` and `password` with your actual username and password.
**Test your client configuration:** Run the client from the AppVM XTerm terminal in the 'vpn'
openvpn --cd /rw/config/vpn --config openvpn-client.ovpn
Watch for status messages that indicate whether the connection is successful (e.g. `Initialization Sequence Completed`) and test from another VPN VM terminal window with ping.
ping 1.1.1.1
`ping` can be aborted by pressing the two key combination `Ctrl+C`. DNS may be tested at this point by replacing addresses in `/etc/resolv.conf` with ones appropriate for your VPN (although this file will not be used when setup is complete, therefore revert to your original qubes nameservers after testing the connection). Diagnose any connection problems using resources such as client documentation and help from your VPN service provider. Proceed to the next step when you're sure the basic VPN connection is working.
nano /rw/config/vpn/qubes-vpn-handler.sh
Add the following:
#!/bin/bash
set -e
export PATH="$PATH:/usr/sbin:/sbin"
case "$1" in
up)
# To override DHCP DNS, assign DNS addresses to 'vpn_dns' env variable before calling this script;
# Format is 'X.X.X.X Y.Y.Y.Y [...]'
if [[ -z "$vpn_dns" ]] ; then
# Parses DHCP foreign_option_* vars to automatically set DNS address translation:
for optionname in ${!foreign_option_*} ; do
option="${!optionname}"
unset fops; fops=($option)
if [ ${fops[1]} == "DNS" ] ; then vpn_dns="$vpn_dns ${fops[2]}" ; fi
done
fi
nft flush chain inet user-vpn prerouting-nat
nft flush chain inet user-vpn forward-filter-dns
if [[ -n "$vpn_dns" ]] ; then
# Set DNS address translation in firewall:
for addr in $vpn_dns; do
nft add rule inet user-vpn prerouting-nat iifgroup 2 udp dport 53 counter dnat ip to $addr
nft insert rule inet user-vpn forward-filter-dns iifgroup 2 udp dport 53 ip daddr $addr counter accept
nft add rule inet user-vpn prerouting-nat iifgroup 2 tcp dport 53 counter dnat ip to $addr
nft insert rule inet user-vpn forward-filter-dns iifgroup 2 tcp dport 53 ip daddr $addr counter accept
done
su - -c 'notify-send "LINK IS UP." --icon=network-idle' user
else
su - -c 'notify-send "LINK UP, NO DNS!" --icon=dialog-error' user
fi
;;
down)
su - -c 'notify-send "LINK IS DOWN !" --icon=dialog-error' user
# Restart the VPN automatically
sleep 5s
sudo /rw/config/rc.local
;;
esac
Save the script (ctrl+x, y, and Enter). Make it executable.
chmod +x /rw/config/vpn/qubes-vpn-handler.sh
nano /rw/config/vpn/openvpn-client.ovpnAdd the following under the redirect-gateway def1 line:
script-security 2
up 'qubes-vpn-handler.sh up'
down 'qubes-vpn-handler.sh down'
Remove other instances of lines starting with script-security, up or down should there be any others.
Save the script (ctrl+x, y, and Enter)
nano /rw/config/vpn/qubes-vpn-firewall.sh
Add the following:
#!/bin/bash
set -e
# Add the `qvpn` group to system. The sync and sleep were in the original script but might not be needed.
groupadd -rf qvpn; sync; sleep 2s
nft add table inet user-vpn
nft 'add chain inet user-vpn forward-filter { type filter hook forward priority filter - 1; policy accept; }'
# Block forwarding of connections through upstream network device (in case the vpn tunnel breaks or packets are routed
# directly between upstream and tunnel interface).
nft add rule inet user-vpn forward-filter meta iifgroup 1 counter drop
nft add rule inet user-vpn forward-filter meta oifgroup 1 counter drop
nft add rule inet user-vpn forward-filter meta mark set 3000 counter
nft add chain inet user-vpn forward-filter-dns
nft add rule inet user-vpn forward-filter jump forward-filter-dns
# The up script adds a DNAT rule to prerouting-nat and a corresponding accept rule to forward-filter-dns. If the accept
# rule is not hit then the DNAT was not applied and the packet should be dropped else it will go through the tunnel with
# the AppVM DNS (DNS leak). This could happen if the tunnel is created after a packet passes prerouting-nat but before
# the routing decision is made. If Qubes adds a higher priority DNAT DNS rule then DNS will stop working but not leak.
nft add rule inet user-vpn forward-filter iifgroup 2 udp dport 53 counter drop
nft add rule inet user-vpn forward-filter iifgroup 2 tcp dport 53 counter drop
nft 'add chain inet user-vpn postrouting-filter { type filter hook postrouting priority filter - 1; policy accept; }'
# Tunnel broke after forward-filter and the reroute check redirected packet to oifgroup 1.
nft add rule inet user-vpn postrouting-filter meta mark 3000 oifgroup 1 counter drop
# Drop all output through the tunnel interface and only allow traffic from the `qvpn` group to the uplink interface. Our
# VPN client will run with group `qvpn`.
nft 'add chain inet user-vpn output-filter { type filter hook output priority filter - 1; policy drop; }'
nft add rule inet user-vpn output-filter meta oifgroup 1 skgid qvpn counter accept
nft add rule inet user-vpn output-filter meta oifgroup 2 counter accept
nft add rule inet user-vpn output-filter meta oifname "lo" counter accept
nft add rule inet user-vpn output-filter counter
# Chain for up script.
nft 'add chain inet user-vpn prerouting-nat { type nat hook prerouting priority dstnat - 1; policy accept; }'
# Rely on Qubes rules for masquerade.
# Rely on Qubes rules for preventing inbound packets (they're dropped unless established,related).
Save the script (ctrl+x, y, and Enter). Make it executable.
chmod +x /rw/config/vpn/qubes-vpn-firewall.sh
nano /rw/config/rc.localClear out the existing lines and add:
#!/bin/bash
export VPN_CLIENT='openvpn'
VPN_OPTIONS='--cd /rw/config/vpn/ --config openvpn-client.ovpn --daemon'
# Uncomment to randomly select a .ovpn config from /rw/config/vpn/
#VPN_OPTIONS='--cd /rw/config/vpn/ --config '$(find /rw/config/vpn/*.ovpn|shuf -n1|rev|cut -d/ -f1|rev)' --daemon'
groupadd -rf qvpn ; sleep 2s
sg qvpn -c "$VPN_CLIENT $VPN_OPTIONS"
su - --whitelist-environment=VPN_CLIENT -c 'notify-send "Starting $VPN_CLIENT..." --icon=network-idle' user
<small> âšī¸ </small> Optional: If you have multiple .ovpn configuration files inside
/rw/config/vpn/and want to start a random VPN connection each time the VPN Gateway boots up, comment the 3rd line and uncomment the 6th line
Save the script (ctrl+x, y, and Enter) and close the terminal.
Shutdown the VPN VM. In dom0
qvm-shutdown sys-vpn
Create a systemd service in VPN template
Start the OpenVPN template and spawn XTerm terminal
qvm-run -u root debian-13-minimal-ovpn xterm
Create the systemd service
nano /lib/systemd/system/qubes-vpn-firewall.service
Add the following:
[Unit]
Description=Qubes VPN firewall updater
After=qubes-firewall.service
Before=qubes-network.service
[Service]
type=oneshot
ExecStart=/rw/config/vpn/qubes-vpn-firewall.sh
[Install]
RequiredBy=qubes-network.service
ctrl+x, y, and Enter)
> <small> **Note:** This system service ensures the nftables rules from `/rw/config/vpn/qubes-vpn-firewall.sh` are in place *after* the `qubes-firewall.service` is active and *before* the `qubes-network.service` has started. </small>
Enable the service
systemctl enable qubes-vpn-firewall.service
Close the terminal.
Shutdown the VPN template. In dom0
qvm-shutdown debian-13-minimal-ovpn
Start your VPN Gateway VM. You should see VPN Notifications on the top right corner of your monitor.
You can now use your VPN Gateway! :partying_face: Set sys-vpn as the Net Qube for your AppVMs.
A highly recommended extra layer of anti-leak firewall rules can be set in dom0 for the VPN Gateway VM. These rules are outside the scope of the VPN VM and would require dom0 compromise in order to be tampered with.
<small> â ī¸ </small> Make sure the VPN Gateway (sys-vpn) is turned off before proceeding.
In dom0 terminal run the following commands â replace <> placeholders with the connection information of your OpenVPN configuration
qvm-firewall <VPN-Gateway-VM-Name> reset
qvm-firewall <VPN-Gateway-VM-Name> add accept <OpenVPN-server-IP>/32 <protocol> <port>
qvm-firewall <VPN-Gateway-VM-Name> add drop
qvm-firewall <VPN-Gateway-VM-Name> del --rule-no 0 # Delete default 'accept' rule
qvm-firewall sys-vpn reset
qvm-firewall sys-vpn add accept 123.45.67.89/32 udp 1194
qvm-firewall sys-vpn add drop
qvm-firewall sys-vpn del --rule-no 0
Check the firewall rules with
qvm-firewall sys-vpn list
It should look like
user@dom0:~$ qvm-firewall sys-vpn list
NO ACTION HOST PROTOCOL PORT(S) SPECIAL TARGET ICMP TYPE EXPIRE COMMENT
0 accept 123.45.67.89/32 udp 1194 - - - -
1 drop - - - - - - -
To prevent recording OpenVPN (client) logs in the Gateway VM, add the following to your .ovpn configuration file:
log /dev/null
status /dev/null
verb 0
To debug your OpenVPN connection, add the following to your .ovpn config:
verb 3
log /home/user/log
This will save the verbose connection log to the /home/user/log file.