QubesOS Web Browser Proxies

Reading Time: ~14 minute(s)

My sincerest thanks to Dave of d10.dev for a thorough review of this piece and his excellent posts and tools for QubesOS over the years.

Browsing the web is the most dangerous thing you do with a computer.

We use electronic banking with the same browser we use to read the news, doom scroll, and splunk on Wikipedia. What if something happens to our web browser?

A simple solution is to install two web browsers – say Chrome and Firefox – or use two VMs. Chrome is only for banking and Firefox is for everything else. “Fixed” you say.

Two problems:

  1. We don’t trust the web sites we’re visiting entirely, even our bank’s or web mail providers’. They could be hacked and distribute malware and we could end up with a web browser that installs ransomware or mines Bitcoin.

  2. We’re human and we make mistakes. We contaminate our web browser meant only for banking. We click on bad links, ignore security warnings, and put login credentials into questionable web sites.

Qubes can help us with #1 by limiting the blast radius of a web browser being exploited. But help for “being only human”? Yes we can do it! Click wildly, browse freely, and let the computer sort it out.


What does this look like?

We map our workflows to specific AppVMs and limit them to only the level of netork access they require to do their jobs.

tldr; AppVM networking is enabled but with everything denied and the default gateway provides a filtering proxy (tinyproxy) as the only egress.

Proxy Unaware Traffic Dropped

Without using the proxy traffic simply doesn’t flow.

user@web:~$ wget http://google.com
--2021-06-12 22:42:40--  http://google.com/
Resolving google.com (google.com)..., 2607:f8b0:4005:80f::200e
Connecting to google.com (google.com)||:80... failed: No route to host.
Connecting to google.com (google.com)|2607:f8b0:4005:80f::200e|:80... failed: Network is unreachable.
user@web:~$ traceroute google.com
traceroute to google.com (, 30 hops max, 60 byte packets
 1 (  0.538 ms  0.505 ms  0.492 ms
 2 (  0.481 ms !X  0.464 ms !X  0.452 ms !X

Note: DNS works – because it is passed by default if networking is enabled at all for an AppVM. But there is no path for other traffic from the AppVM to the Internet without going via the proxy.

Proxy Aware Traffic Filtered

This example filtering proxy is configured to allow access to Github and nothing else.

user@web:~$ http_proxy= wget http://github.com -O/dev/null
--2022-03-20 00:20:53--  http://github.com/
Connecting to connected.
Proxy request sent, awaiting response... 301 Moved Permanently
Location: https://github.com/ [following]
--2022-03-20 00:20:54--  https://github.com/
Connecting to connected.
Proxy request sent, awaiting response... 200 OK
<snip saving file>

Github access is allowed. It redirects to the HTTPS version of the site and downloads index.html.

user@web:~$ http_proxy= wget http://google.com
--2022-03-20 00:20:32--  http://google.com/
Connecting to connected.
Proxy request sent, awaiting response... 403 Filtered
2022-03-20 00:20:32 ERROR 403: Filtered.

But Google access is not allowed and is returned a ‘Permission Denied’ status code.


ProxyVM Setup

Given the wide use of proxys for web filtering it should be of little surprise they are our best option for creating web browsing compartments. See QubesOS Options for Controlling Egress for paths not taken (or taken and severely regretted).

Note: Qubes atttempts to provide domain filtering functionality via its’ qubes-firewall however in it all domain names (e.g. gmail.com) are immediately resolved and stored as IP addresses. Unfortunately the IP addresses used by many web services change over time and by location. This results in a frustrating web experience as pages get blocked because the firewall rules don’t map to the same IPs returned for the same name at a different time and location. This has been a driving motivation for the solution laid out below.

Create Template for ProxyVMs

Below I lay out how to create a minimal VM in Qubes for use as a ‘ProxyVM’.

sudo qubes-dom0-update qubes-template-debian-11-minimal

This takes a few minutes to download (~200MB) and install and will start and stop the VM to initially configure it (assign static IPs etc).

Start the new VM and specify the root user since minimal VM templates do not come with sudo configured.

qvm-run -u root debian-11-minimal xterm

Note: To paste into an xterm use ‘Shift + Insert’

Update the new template VM:

apt update
apt list --upgradable
apt upgrade
apt autoremove

I like to keep a clean copy of templates so I can always start over.

Always shutdown a template VM to ensure changes are saved to its image before doing operations like cloning or making a new AppVM based on it.

from Dom0:

qvm-shutdown debian-11-minimal
qvm-clone debian-11-minimal debian-11-proxy

This consumes extra disk space and is another template to keep up to date (but only if you use both). Skip it if desired and simply read debian-11-proxy below as debian-11-minimal.

Setup Tinyproxy in TemplateVM

Get a shell in the new ProxyVM template:

qvm-run -u root debian-11-proxy xterm

Install packages required to act as a ProxyVM:

apt install qubes-core-agent-networking nftables tinyproxy

Qubes will disable tinyproxy by default as it normally uses tinyproxy for it’s update proxy (to update template VMs).

# note it is disabled by the file
# /usr/lib/systemd/system/tinyproxy.service.d/30_not_needed_in_qubes_by_default.conf
systemctl status tinyproxy

You can leave that file alone – we can enable it using Qubes Settings on a per VM basis in the Qube Manager.

More important is how we can vary proxy configuration between different proxy VMs. Rather than changing the configuration inside the VM template we only need to change how tinyproxy finds its config when started. And then we can keep per-VM configuration separate from the base proxy image.

We can do this by editing the default environment used by systemd to start tinyproxy. And if we point tinyproxy at a config in /rw every ProxyVM can have different configuration and filtering rules.

In debian-11-proxy edit /etc/default/tinyproxy and add the following line:

FLAGS="-c /rw/config/tinyproxy/tinyproxy.conf"

You can review the tinyproxy systemd config at /lib/systemd/system/tinyproxy.service to see how the defaults file is used.

Configure the TemplateVM as a Qubes Firewall VM

Setting this is required to control firewall rules of attached VMs (the feature is inherited by AppVMs using the template so it should not need to be set on individual ProxyVMs).

qvm-features debian-11-proxy qubes-firewall 1
# confirm
qvm-features debian-11-proxy qubes-firewall

Create ProxyVM in Dom0

Create the first ProxyVM:

qvm-create --template debian-11-proxy --label green proxy-bank
qvm-run -u root proxy-bank xterm

Finish Tinyproxy Setup in ProxyVM

Unfortunately I haven’t found a good way to seed /rw directories on an initial basis from TemplateVMs. So to make tinyproxy work on the new proxy-bank AppVM we must put a config at /rw/config/tinyproxy/tinyproxy.conf.

mkdir /rw/config/tinyproxy
# Run the above to compare the default config with the modified one below
cat /etc/tinyproxy/tinyproxy.conf | egrep -v '^#|^$'
# You can copy your own tinyproxy.conf and modify it but pay special attention
# to copy the Listen, Allow and Filter* lines.

Put this in /rw/config/tinyproxy/tinyproxy.conf:

User tinyproxy
Group tinyproxy
Port 8888
Timeout 600
ErrorFile 403 "/usr/share/tinyproxy/403.html"
DefaultErrorFile "/usr/share/tinyproxy/default.html"
LogFile "/var/log/tinyproxy/tinyproxy.log"
LogLevel Info
PidFile "/run/tinyproxy/tinyproxy.pid"
MaxClients 100
MinSpareServers 5
MaxSpareServers 20
StartServers 10
DisableViaHeader Yes
Filter "/rw/config/tinyproxy/filter"
FilterExtended On
FilterDefaultDeny Yes
ConnectPort 443

Note: Be sure and review your Qubes AppVMs IP address range – I have gotten reports of in use instead of the above in some configurations.

This is the default config modified to add filtering, use the Qubes IP address range, and to only allow port 443 (HTTPS) for CONNECT (the HTTP verb used to set up TCP socket forwarding, usually for encrypted traffic).

Since we reference the filter file in the tinyproxy config it must also exist. Copy the below lines into /rw/config/tinyproxy/filter:

# Specify domains to allow below using extended regex.
# Mozilla Add-ons
# Debian updates

This allows access to the Mozilla add-ons site to install plugins for Firefox.

Whenever the above filter changes tinyproxy needs a reload – a soft reload may be done by running killall -HUP tinyproxy as root.

Version 1.11 of tinyproxy (in bullseye backports) support extremely soft reloads via SIGUSR1 where existing connections are not interrupted (though not usually a huge deal for a single AppVM).

See Appendix for additional filter file clauses for commonly used sites.

Finish the ‘On-Host’ ProxyVM Setup

With generic tinyproxy configuration is in place. Next we must make it possible for client AppVMs to reach tinyproxy.

Normally VMs in Qubes, even those acting as the network provider for other VMs have a strict firewall in place. This firewall enforces VM isolation by dropping traffic between client VMs and only allows traffic out toward the Internet.

In order to use our ProxyVMs we have to poke a hole in this firewall to allow client VMs to access the proxy. This means modifying the INPUT chain to allow traffic to tinyproxy. Normally the only traffic from an AppVM to a NetVM would be handled on the FORWARD chain.

View the existing INPUT chain on the default (‘filter’) table:

iptables -L INPUT -v -n --line-numbers

Find line, probably 2nd from the bottom, on INPUT chain directly above REJECT target reject-with icmp-host-prohibited message – and use it’s line number (the REJECT will be bumped down) – it is 6 for me.

iptables -I INPUT 6 -p tcp -s --dport 8888 -j ACCEPT

Review the INPUT chain to ensure it looks good and make it permanent by adding the same line as above (if different than mine) to /rw/config/qubes-firewall-user-script:

iptables -I INPUT 6 -p tcp -s --dport 8888 -j ACCEPT

Ensure the firewall script is enabled with:

chmod a+x /rw/config/qubes-firewall-user-script

Shutdown the new ProxyVM so we can restart it and ensure everything comes up fine without intervention:

shutdown -h now

Finish ProxyVM Setup in Dom0

# Enables proxy to forward network traffic
qvm-prefs --set proxy-bank provides_network True
# confirm it was set
qvm-prefs --get proxy-bank provides_network
# enables tinyproxy to start
qvm-service --enable proxy-bank tinyproxy
# confirm it was set
qvm-service --list proxy-bank

Use Qubes Manager to setup the following firewall rules for the new proxy VM:

Verify the firewall of the ProxyVM in Dom0 by running:

qvm-firewall proxy-bank list

Start-up the ProxyVM for Testing

Start the new ProxyVM to ensure it starts with the new Qubes preferences and services in place.

From Dom0 start a root shell on the ProxyVM:

qvm-run -u root proxy-bank xterm

On the ProxyVM, check tinyproxy is running + that port 8888 is allowed in the INPUT chain:

systemctl status tinyproxy
journalctl -u tinyproxy
iptables -L -n -v

Configuring a Client AppVM

One option is to create a new client VM using the ProxyVM. This creates a new VM called banking from the debian-11 template and uses proxy-bank to provide the network:

qvm-create --template debian-11 --label yellow --property netvm=proxy-bank banking

Another option is to reconfigure an existing VM either in the ‘Qubes Settings’ for that VM or on the command line as follows:

qvm-prefs --set $my_appvm netvm proxy-bank

A VM reconfigured this way will need to be restarted to pick up the changes.


You must configure the new client AppVMs to have networking enabled but all connections denied.

Only traffic sent to tinyproxy can be filtered. Using the FORWARD firewall chain, aka what is configured in Qube Settings, will bypass tinyproxy entirely as the ProxyVM will be acting like a packet router and passing traffic.

Remember, the ProxyVM is not really in control of its’ network forwarding table – the qubes-firewall is in control and the ProxyVM will forward whatever the qubes-firewall has configured.

To ensure your client AppVMs cannot simply go around the ProxyVM by not configuring a proxy, configure the qubes-firewall to deny all traffic for each client AppVM.

This keeps networking enabled but no specific connections are allowed. All traffic sent from the client AppVM to the ProxyVM with the intention of being forwarded will be dropped. But traffic addressed directly to our ProxyVM from the client VM is allowed, and is how tinyproxy filtering works.

Using the Proxy

To configure everything to use the ProxyVM we need to get its IP address. We can use that to point our web browser at the proxy, setup our terminal environment to use the proxy by default, and configure our package manager to use the proxy (needed for a StandaloneVM).

Configure Firefox

Configure Your Terminal

Add the following to the bottom of your $HOME/.bashrc or other shell configuration to more easily use the proxy at the command line:

PROXY="http://$(ip route get| awk '/via/ {print $3}'):8888"
export HTTP_PROXY HTTPS_PROXY http_proxy https_proxy

Apply the change to your current shell by ‘sourcing’ the file:

source ~/.bashrc

Configure Debian Package Manager (APT)

Note: This primarily useful for a StandaloneVM but not your TemplateVMs. TemplateVMs use a different tinyproxy configuration in Qubes to access update servers.

To use the proxy, create a file in the ‘drop folder’ for apt package manager.

Create /etc/apt/apt.conf.d/80proxy using the below as a template. And replace the IP address below with your ProxyVMs IP:

Acquire::https::proxy "";
Acquire::http::proxy "";
Acquire::ftp::proxy "";

Configure Outbound Ports Not via the Proxy

This can be useful for setting up local mail clients which may need ports for sending and receiving email. Whatever your use case the pattern is the same.

Using the example port of 993 (IMAPS) do the following to enable traffic to flow from your client AppVM through your ProxyVM (and not via tinyproxy).

The above will create the firewall rule in your ProxyVM for your client VM to access the specified email server. However your ProxyVM itself is not allowed to access the email server on port 993 because its NetVM doesn’t allow it.

To permit the ProxyVM to pass the traffic on-ward follow the same steps again but for the ProxyVM this time. By creating the rules in both the ProxyVM and the ProxyVMs’ network provider you can bypass the proxy for unsupported protocols.

Always remember creating firewall rules for a VM creates the rules in the VMs’ network provider and not in the VM itself.

Common Operations - Adding New Sites

Troubleshooting Tips


Appendix - Filter file snippets



Alpine Linux

Often needed for updating Alpine Docker images







Packages may be hosted anywhere expect to have some issues downloading and be prepared to extend this list.



Packages may be hosted anywhere expect to have some issues downloading and be prepared to extend this list.


Anti list

Things I didn’t mean to do but needed to anyway.

browse templates under -itl repos here


upgrade vm kernel

sudo qubes-dom0-update --best --allowerasing kernel-qubes-vm

upgrade dom0 kernel

sudo qubes-dom0-update --best --allowerasing kernel

This will normally keep 3 versions for both of these and will remove the oldest version.

clean out old templates

dnf remove qubes-template-fedora-30-minimal


Opinions expressed on this site are my own.