Hardened Qubes VPN ProxyVM support for Qubes 4.3, nftables, strict DNS, and IPv6 disablement revisions

Go back to topic: Hardened Qubes VPN ProxyVM support for Qubes 4.3, nftables, strict DNS, and IPv6 disablement

  1. v3 anchor; v3 full version
  2. v2 anchor; v2 full version

Revision #3

Edited on
2026-04-24
Edited by user
demosios
This work is based on the original Qubes VPN support project by @tasket. That project provided the core model and much of the structure for running a VPN client inside a dedicated Qubes ProxyVM. Credit belongs to @tasket for the original design and implementation. I also want to credit @1choice for laying important groundwork for the shift toward `nftables`. The goal of this fork is to update and harden the project for a modern Qubes OS 4.3 setup using `nftables`, with a stricter fail-closed security model. Most existing VPN-in-Qubes approaches either rely heavily on manual firewall configuration, partially documented scripts, NetworkManager behavior, or older `iptables` assumptions. Those can work, but they are often less elegant, less complete, or harder to reason about under failure conditions. This update keeps the Qubes-native ProxyVM model while making the firewall, DNS, IPv6, and startup behavior more explicit. This work is based on the original Qubes VPN support project by @tasket. That project provided the core model and much of the structure for running a VPN client inside a dedicated Qubes ProxyVM. Credit belongs to @tasket for the original design and implementation. I also want to credit @1choice for laying important groundwork for the shift toward `nftables`. The goal of this fork is to update and harden the project for a modern Qubes OS 4.3 setup using `nftables`, with a stricter fail-closed security model. Most existing VPN-in-Qubes approaches either rely heavily on manual firewall configuration, partially documented scripts, NetworkManager behavior, or older `iptables` assumptions. Those can work, but they are often less elegant, less complete, or harder to reason about under failure conditions. This update keeps the Qubes-native ProxyVM model while making the firewall, DNS, IPv6, and startup behavior more explicit.
- VPN DNS handling falls back to QubesDB on Qubes 4.3 where the older helper file is absent - OpenVPN hostname remotes can be resolved through configured VPN DNS before connection - Deployment can be staged so config files, certs, and credentials can be added after ProxyVM creation - Startup checks verify the expected firewall state before the VPN client runs - VPN DNS handling falls back to QubesDB on Qubes 4.3 where the older helper file is absent - OpenVPN hostname remotes can be resolved through configured VPN DNS before connection - WireGuard hostname endpoints can also be resolved through configured VPN DNS before connection - Deployment can be staged so config files, certs, and credentials can be added after ProxyVM creation - Startup checks verify the expected firewall state before the VPN client runs - OpenVPN and WireGuard are now split into explicit backend selections instead of sharing one auto-detected startup path
OpenVPN is the primary supported backend. WireGuard support is included through an optional systemd override selected during ProxyVM initialization, but it should be considered more operationally sensitive because WireGuard is kernel-driven and does not map as cleanly to the same userspace process-group egress model. OpenVPN remains the primary supported backend. WireGuard support is now handled through a dedicated service rather than an optional systemd override. It should still be considered more operationally sensitive because WireGuard is kernel-driven and does not map as cleanly to the same userspace process-group egress model.
- Enable the relevant Qubes service for this handler, if used by your setup - Enable the Qubes service `vpn-handler` if you are using the shipped service model
In the VPN ProxyVM: In the VPN ProxyVM, select the backend explicitly. For OpenVPN:
sudo /usr/lib/qubes/qubes-vpn-setup --config sudo /usr/lib/qubes/qubes-vpn-setup --config-openvpn
This prepares `/rw/config/vpn/` for you. It also asks whether to install the optional WireGuard override: - Answer `y` if this ProxyVM will use WireGuard - Answer `n` or press Enter to keep the default OpenVPN service settings For WireGuard: ```bash sudo /usr/lib/qubes/qubes-vpn-setup --config-wireguard ``` This prepares `/rw/config/vpn/` for you and writes a persistent backend marker for that ProxyVM under one of: ```text /rw/config/vpn/backend-openvpn /rw/config/vpn/backend-wireguard ```
Also place any required certs, keys, CRLs, or other provider files in that directory. If username/password authentication is needed: Also place any required certs, keys, CRLs, or other provider files in that directory. If username/password authentication is needed for OpenVPN:
For the strictest setup, use an IPv4 remote or endpoint address instead of a hostname. If WireGuard was selected during `--config`, the persistent override choice is stored under: ```text /rw/config/qubes-vpn-handler.service.d/10_wg.conf ``` That override is synced into the live systemd drop-in path on boot. For the strictest setup, use an IPv4 remote or endpoint address instead of a hostname. Backend selection is now stored by the persistent marker files under `/rw/config/vpn/`, not by a WireGuard drop-in under `/rw/config/qubes-vpn-handler.service.d/`.
For OpenVPN:
For WireGuard: ```bash sudo systemctl restart qubes-firewall.service sudo systemctl restart qubes-wg-handler.service sudo systemctl status qubes-wg-handler.service ```
ls -l /rw/config/vpn/backend-openvpn /rw/config/vpn/backend-wireguard
- The VPN tunnel interface exists, for example `tun0` - The VPN tunnel interface exists, for example `tun0` for OpenVPN or the WireGuard interface name from the config
- `custom-forward` contains downstream-to-VPN forwarding and stateful return rules - `custom-forward` contains downstream-to-VPN forwarding and stateful return rules
- IPv6 disablement reports `1` Only attach downstream AppVMs to the VPN ProxyVM after verifying the tunnel and DNS rules. - IPv6 disablement reports `1` - Exactly one backend marker exists under `/rw/config/vpn/` Only attach downstream AppVMs to the VPN ProxyVM after verifying the tunnel and DNS rules.
This is security-sensitive networking code. Please test carefully before relying on it for important workloads. This is security-sensitive networking code. Please test carefully before relying on it for important workloads.
- WireGuard startup and reconnect behavior work as expected when the override is enabled - Behavior with non-standard NetVM chains, such as Mirage Firewall or another ProxyVM If you find bugs, edge cases, provider-specific issues, or Qubes-version differences, please open an issue or submit a pull request. The intent is to make this easier to audit and more robust for the community while preserving the basic architecture that made the original `Qubes-vpn-support` project useful.- WireGuard startup and reconnect behavior work as expected with the dedicated `qubes-wg-handler.service` - Behavior with non-standard NetVM chains, such as Mirage Firewall or another ProxyVM If you find bugs, edge cases, provider-specific issues, or Qubes-version differences, please open an issue or submit a pull request. The intent is to make this easier to audit and more robust for the community while preserving the basic architecture that made the original `Qubes-vpn-support` project useful.

Revision #2

Edited on
2026-04-24
Edited by user
demosios
https://github.com/demosios/Qubes-vpn-support --- ## Overview I have published an updated fork of `tasket/Qubes-vpn-support`. This work is based on the original Qubes VPN support project by @tasket. That project provided the core model and much of the structure for running a VPN client inside a dedicated Qubes ProxyVM. Credit belongs to @tasket for the original design and implementation. I also want to credit @1choice for laying important groundwork for the shift toward `nftables`. The goal of this fork is to update and harden the project for a modern Qubes OS 4.3 setup using `nftables`, with a stricter fail-closed security model. Most existing VPN-in-Qubes approaches either rely heavily on manual firewall configuration, partially documented scripts, NetworkManager behavior, or older `iptables` assumptions. Those can work, but they are often less elegant, less complete, or harder to reason about under failure conditions. This update keeps the Qubes-native ProxyVM model while making the firewall, DNS, IPv6, and startup behavior more explicit. --- ## Primary Security Changes - Migration from legacy `iptables` handling to `nftables` - Downstream VMs do not get internet access before the VPN tunnel is up - Downstream forwarding is allowed only toward the VPN tunnel interface - Return traffic is allowed only with `ct state established,related` - Upstream clearnet forwarding from downstream VMs is explicitly blocked - IPv6 is intentionally disabled and dropped, rather than partially supported - Downstream DNS is DNATed only after VPN DNS is available - VPN DNS handling falls back to QubesDB on Qubes 4.3 where the older helper file https://github.com/demosios/Qubes-vpn-support --- ## Overview I have published an updated fork of `tasket/Qubes-vpn-support`. This work is based on the original Qubes VPN support project by @tasket. That project provided the core model and much of the structure for running a VPN client inside a dedicated Qubes ProxyVM. Credit belongs to @tasket for the original design and implementation. I also want to credit @1choice for laying important groundwork for the shift toward `nftables`. The goal of this fork is to update and harden the project for a modern Qubes OS 4.3 setup using `nftables`, with a stricter fail-closed security model. Most existing VPN-in-Qubes approaches either rely heavily on manual firewall configuration, partially documented scripts, NetworkManager behavior, or older `iptables` assumptions. Those can work, but they are often less elegant, less complete, or harder to reason about under failure conditions. This update keeps the Qubes-native ProxyVM model while making the firewall, DNS, IPv6, and startup behavior more explicit. --- ## Primary Security Changes - Migration from legacy `iptables` handling to `nftables` - Downstream VMs do not get internet access before the VPN tunnel is up - Downstream forwarding is allowed only toward the VPN tunnel interface - Return traffic is allowed only with `ct state established,related` - Upstream clearnet forwarding from downstream VMs is explicitly blocked - IPv6 is intentionally disabled and dropped, rather than partially supported - Downstream DNS is DNATed only after VPN DNS is available - VPN DNS handling falls back to QubesDB on Qubes 4.3 where the older helper file
- OpenVPN hostname remotes can be resolved through configured VPN DNS before - OpenVPN hostname remotes can be resolved through configured VPN DNS before
- Deployment can be staged so config files, certs, and credentials can be added - Deployment can be staged so config files, certs, and credentials can be added
- Startup checks verify the expected firewall state before the VPN client runs Full architecture and change documentation: https://github.com/demosios/Qubes-vpn-support/blob/master/README.md --- ## Tested Baseline - Qubes OS 4.3.0 - `debian-13-minimal` template from the Qubes repository - OpenVPN 2.6.14 - `nftables` firewall backend OpenVPN is the primary supported backend. WireGuard support is included through the existing override approach, but it should be considered more operationally sensitive because WireGuard is kernel- driven and does not map as cleanly to the same userspace process-group egress model. --- ## Quickstart ## 1. Install In The TemplateVM In the TemplateVM: ```bash cd Qubes-vpn-support sudo bash ./install ``` Shut down the TemplateVM. --- ## 2. Create The VPN ProxyVM Create a dedicated VPN ProxyVM from that template. In Qubes settings for the VPN ProxyVM: - Enable `provides network` - Set the NetVM as appropriate for your topology - Enable the relevant Qubes service for this handler, if used by your setup Start the VPN ProxyVM. --- ## 3. Initialize The VPN ProxyVM In the VPN ProxyVM: ```bash sudo /usr/lib/qubes/qubes-vpn-setup --config sudo mkdir -p /rw/config/vpn ``` --- ## 4. Add VPN Provider Files Add your provider files to: ```bash /rw/config/vpn/ ``` At minimum, provide: ```bash /rw/config/vpn/vpn-client.conf ``` Also place any required certs, keys, CRLs, or other provider files in that directory. If username/password authentication is needed: ```bash sudo /usr/lib/qubes/qubes-vpn-setup --userpass ``` --- ## 5. Configure Strict DNS For strict hostname handling, configure VPN DNS in the provider config. For OpenVPN: ```conf setenv vpn_dns "X.X.X.X Y.Y.Y.Y" remote vpn.example.net 1194 ``` For the strictest setup, use an IPv4 remote address instead of a hostname. --- ## 6. Start The Service ```bash sudo systemctl restart qubes-firewall.service sudo systemctl restart qubes-vpn-handler.service sudo systemctl status qubes-vpn-handler.service ``` --- ## 7. Verify Before Attaching Downstream VMs ```bash ip -br link sudo nft list chain ip qubes custom-forward sudo nft list chain ip qubes dnat-dns cat /var/run/qubes/qubes-vpn-ns cat /proc/sys/net/ipv6/conf/all/disable_ipv6 ``` Expected high-level results: - The VPN tunnel interface exists, for example `tun0` - The tunnel interface is assigned to group `9` - `custom-forward` contains downstream-to-VPN forwarding and stateful return - Startup checks verify the expected firewall state before the VPN client runs Full architecture and change documentation: https://github.com/demosios/Qubes-vpn-support/blob/master/README.md --- ## Tested Baseline - Qubes OS 4.3.0 - `debian-13-minimal` template from the Qubes repository - OpenVPN 2.6.14 - `nftables` firewall backend OpenVPN is the primary supported backend. WireGuard support is included through an optional systemd override selected during ProxyVM initialization, but it should be considered more operationally sensitive because WireGuard is kernel-driven and does not map as cleanly to the same userspace process-group egress model. --- ## Quickstart ## 1. Install In The TemplateVM In the TemplateVM: ```bash cd Qubes-vpn-support sudo bash ./install ``` Shut down the TemplateVM. --- ## 2. Create The VPN ProxyVM Create a dedicated VPN ProxyVM from that template. In Qubes settings for the VPN ProxyVM: - Enable `provides network` - Set the NetVM as appropriate for your topology - Enable the relevant Qubes service for this handler, if used by your setup Start the VPN ProxyVM. --- ## 3. Initialize The VPN ProxyVM In the VPN ProxyVM: ```bash sudo /usr/lib/qubes/qubes-vpn-setup --config ``` This prepares `/rw/config/vpn/` for you. It also asks whether to install the optional WireGuard override: - Answer `y` if this ProxyVM will use WireGuard - Answer `n` or press Enter to keep the default OpenVPN service settings --- ## 4. Add VPN Provider Files Add your provider files to: ```bash /rw/config/vpn/ ``` At minimum, provide: ```bash /rw/config/vpn/vpn-client.conf ``` Also place any required certs, keys, CRLs, or other provider files in that directory. If username/password authentication is needed: ```bash sudo /usr/lib/qubes/qubes-vpn-setup --userpass ``` For WireGuard, `vpn-client.conf` should be a `wg-quick` style config. --- ## 5. Configure Strict DNS For strict hostname handling, configure VPN DNS in the provider config. For OpenVPN: ```conf setenv vpn_dns "X.X.X.X Y.Y.Y.Y" remote vpn.example.net 1194 ``` For WireGuard: ```ini DNS = X.X.X.X, Y.Y.Y.Y Endpoint = vpn.example.net:51820 ``` For the strictest setup, use an IPv4 remote or endpoint address instead of a hostname. If WireGuard was selected during `--config`, the persistent override choice is stored under: ```text /rw/config/qubes-vpn-handler.service.d/10_wg.conf ``` That override is synced into the live systemd drop-in path on boot. --- ## 6. Start The Service ```bash sudo systemctl restart qubes-firewall.service sudo systemctl restart qubes-vpn-handler.service sudo systemctl status qubes-vpn-handler.service ``` --- ## 7. Verify Before Attaching Downstream VMs ```bash ip -br link sudo nft list chain ip qubes custom-forward sudo nft list chain ip qubes dnat-dns cat /var/run/qubes/qubes-vpn-ns cat /proc/sys/net/ipv6/conf/all/disable_ipv6 ``` Expected high-level results: - The VPN tunnel interface exists, for example `tun0` - The tunnel interface is assigned to group `9` - `custom-forward` contains downstream-to-VPN forwarding and stateful return
- `dnat-dns` contains DNS DNAT rules after the VPN is up - `/var/run/qubes/qubes-vpn-ns` contains the VPN DNS values - IPv6 disablement reports `1` Only attach downstream AppVMs to the VPN ProxyVM after verifying the tunnel and DNS rules. --- ## Testing And Feedback This is security-sensitive networking code. Please test carefully before relying on it for important workloads. Useful things to test: - Downstream VM has no internet before VPN connection - Downstream DNS does not work before VPN connection - Downstream traffic works after VPN connection - Downstream DNS is redirected to VPN DNS after connection - Traffic stops when the VPN service is stopped or restarted - IPv6 is unavailable - OpenVPN reconnects behave as expected - Behavior with non-standard NetVM chains, such as Mirage Firewall or another - `dnat-dns` contains DNS DNAT rules after the VPN is up - `/var/run/qubes/qubes-vpn-ns` contains the VPN DNS values - IPv6 disablement reports `1` Only attach downstream AppVMs to the VPN ProxyVM after verifying the tunnel and DNS rules. --- ## Testing And Feedback This is security-sensitive networking code. Please test carefully before relying on it for important workloads. Useful things to test: - Downstream VM has no internet before VPN connection - Downstream DNS does not work before VPN connection - Downstream traffic works after VPN connection - Downstream DNS is redirected to VPN DNS after connection - Traffic stops when the VPN service is stopped or restarted - IPv6 is unavailable - OpenVPN reconnects behave as expected - WireGuard startup and reconnect behavior work as expected when the override is enabled - Behavior with non-standard NetVM chains, such as Mirage Firewall or another
If you find bugs, edge cases, provider-specific issues, or Qubes-version differences, please open an issue or submit a pull request. The intent is to make this easier to audit and more robust for the community while preserving the basic architecture that made the original `Qubes-vpn-support` project useful.If you find bugs, edge cases, provider-specific issues, or Qubes-version differences, please open an issue or submit a pull request. The intent is to make this easier to audit and more robust for the community while preserving the basic architecture that made the original `Qubes-vpn-support` project useful.