The definitive playbook for zero-contamination browsing.
Step-by-step playbook for creating a completely isolated, VPN-routed environment in your homelab. Each phase builds on the last — don't skip ahead or reorder steps.
By handling the routing at the pfSense level and isolating the browser session within Proxmox and Docker, you guarantee zero cross-contamination with your main daily driver's cookies, cache, or MAC address.
192.168.1.0/24 — substitute your own subnet wherever you see it.Instead of running a desktop VPN client that could leak, we create a dedicated WireGuard tunnel directly on the router. WireGuard operates in the kernel, has a much smaller attack surface, and delivers 2–3× the throughput of OpenVPN for equivalent hardware.
| Field | Value | Notes |
|---|---|---|
| Name | BURNER_IP | No spaces — this exact name is referenced in every rule that follows |
| Type | Host(s) | Not "Network" — you're aliasing a single host |
| IP or FQDN | 192.168.1.50 | An unused static IP on your LAN. Must match the IP you assign to the Docker container in Phase 4. |
| Description | Burner browser container IP | — |
192.168.1.50 is outside your DHCP pool before proceeding: check Services → DHCP Server → LAN and confirm the "Range" fields do not include .50..conf file — it will look like:
[Interface]
PrivateKey = <YOUR_PRIVATE_KEY>
Address = 10.2.0.2/32
DNS = 10.2.0.1
[Peer]
PublicKey = <SERVER_PUBLIC_KEY>
AllowedIPs = 0.0.0.0/0
Endpoint = <SERVER_IP>:51820
Keep this file open — every value maps directly to a pfSense field.
| pfSense Field | Value | Source |
|---|---|---|
| Description | ProtonVPN | Your choice |
| Listen Port | 51820 | Standard WireGuard port |
| Private Key | Paste from [Interface] PrivateKey | Your .conf file |
| Interface Address | 10.2.0.2/32 | [Interface] Address |
| pfSense Field | Value | Source |
|---|---|---|
| Tunnel | Select the tunnel you just created | — |
| Public Key | Paste from [Peer] PublicKey | Your .conf file |
| Pre-shared Key | Leave blank | ProtonVPN doesn't use PSKs by default |
| Allowed IPs | 0.0.0.0/0 | Routes all traffic through the tunnel |
| Endpoint | IP portion of [Peer] Endpoint | Your .conf file |
| Endpoint Port | 51820 | Port portion of Endpoint |
| Keep Alive | 25 | Maintains tunnel through NAT |
tun_wg0 in the dropdown and click + Add. Click the new interface (OPT1 or similar) to edit:
| Field | Value | Why |
|---|---|---|
| Enable | ✅ Checked | Disabled by default |
| Description | PROTON_VPN | Referenced by name in every rule |
| IPv4 Configuration Type | Static IPv4 | Must be Static — "None" prevents gateway creation |
| IPv4 Address | IP from [Interface] Address (e.g. 10.2.0.2) | Tunnel's local endpoint |
| Subnet mask | 32 | ProtonVPN assigns a /32 point-to-point address |
| IPv6 Configuration Type | None | IPv6 on WireGuard requires separate config |
| Block Private Networks | ❌ Unchecked | VPN peer uses RFC1918 — blocking breaks the tunnel |
| MTU | 1420 | Standard for WireGuard over UDP |
| Field | Value | Notes |
|---|---|---|
| Interface | PROTON_VPN | The interface you just created |
| Protocol | any | — |
| Source | Single host or alias → BURNER_IP | Use the alias — pfSense resolves at evaluation time |
| Destination | any | — |
| Translation → Address | Interface Address | NATs the burner traffic using the WireGuard tunnel's IP |
| Static Port | ❌ Unchecked | Only needed for VoIP/gaming |
| Description | Burner → ProtonVPN | — |
| Field | Value | Notes |
|---|---|---|
| Interface | PROTON_VPN | — |
| Name | PROTON_VPN_GW | Exact name referenced in the policy routing rule |
| Gateway IP | 10.2.0.1 | DNS value from [Interface] — the far end of the WireGuard tunnel. Not the public Endpoint IP. |
| Monitor IP | 10.2.0.1 | pfSense pings this to determine tunnel health |
| State Killing on Gateway Failure | Kill states for down gateways | Forces pfSense to drop all connections when VPN goes down — not let them linger on WAN |
| Use non-local gateway (Advanced) | ✅ Check this | Required with /32 interface address — pfSense would otherwise reject 10.2.0.1 as outside the interface subnet |
Even if your burner IP routes all traffic through the VPN, DNS queries are the most common leakage vector. If the container resolves names through your Pi-hole (which forwards to your ISP), your ISP can see every domain you visit. This phase eliminates that by forcing DNS through the VPN's own resolver.
.conf file. Look for the DNS = line in the [Interface] block:
[Interface]
...
DNS = 10.2.0.1 ← this is the value you need
10.2.0.1 is common but not universal — always read it from your specific config file. If you regenerate the config for a different server, re-check this value.10.2.0.1
. You will use it in the Docker Compose file and the NAT rule below.
| Field | Value | Notes |
|---|---|---|
| Interface | LAN | The interface your Proxmox host is on |
| Protocol | TCP/UDP | DNS uses UDP 53 primarily, TCP 53 for large responses |
| Source (click Advanced) | Single host → BURNER_IP | Only redirect DNS from this specific host |
| Destination | any | Intercepts regardless of which DNS server the container tries to reach |
| Destination Port | 53 | Standard DNS port |
| Redirect Target IP | 10.2.0.1 | Your VPN DNS server from above |
| Redirect Target Port | 53 | — |
| NAT Reflection | Disable | Must be explicitly disabled with "Any" destination — leaving it on may cause a save error |
| Filter rule association | Add associated filter rule | Auto-creates the firewall PASS rule for the redirected traffic |
Policy-based routing tells pfSense to make a routing decision based on the source IP, overriding the default routing table. Only your burner IP gets forced through the ProtonVPN gateway — the rest of your network is completely unaffected.
| Field | Value | Notes |
|---|---|---|
| Action | Pass | — |
| Interface | LAN | — |
| Protocol | any | — |
| Source | Single host → BURNER_IP | Use the alias |
| Destination | any | — |
| Description | Route Burner via ProtonVPN | — |
| Advanced Field | Value | Why |
|---|---|---|
| Gateway | PROTON_VPN_GW | This is the policy routing override — the core of this entire setup. Without it, traffic routes via WAN. |
| Disable reply-to | ✅ Check this | Prevents pfSense from sending return traffic back out the wrong interface — asymmetric routing breaks TCP connections |
| Field | Value | Notes |
|---|---|---|
| Action | Block | Silently drops packets — Reject would send a TCP RST back to the container |
| Interface | LAN | — |
| Protocol | any | — |
| Source | Single host → BURNER_IP | Same source as the Pass rule above |
| Destination | any | — |
| Description | Kill Switch — block burner if VPN down | — |
Instead of an Incognito tab, we deploy a fully self-contained browser inside a Docker container using a macvlan network. This gives the container its own MAC address and its own IP on your LAN — from pfSense's perspective, it looks like a completely separate physical device.
| Field | Value | Notes |
|---|---|---|
| Name | burner-browser | — |
| Field | Value | Notes |
|---|---|---|
| ISO Image | Debian 12 or Ubuntu 24.04 LTS | Minimal/server install — no desktop environment needed |
| Guest OS Type | Linux | — |
| Field | Value | Notes |
|---|---|---|
| Machine | q35 | Preferred for modern VMs |
| Qemu Agent | ✅ Enabled | Install qemu-guest-agent inside the VM after OS install |
| Field | Value | Notes |
|---|---|---|
| Disk size | 10 GiB | Sufficient for OS, Docker, and the Firefox image |
| Discard | ✅ Enabled | Enables TRIM if storage is SSD/NVMe-backed |
| Field | Value | Notes |
|---|---|---|
| Cores | 2 | Sufficient for a single browser container |
| Type | host | Best performance — passes through host CPU flags |
| Field | Value | Notes |
|---|---|---|
| Memory | 2048 MiB | Firefox under noVNC uses ~1–1.5 GB under load |
| Field | Value | Notes |
|---|---|---|
| Bridge | vmbr0 | Same bridge your LAN devices are on |
| Model | VirtIO (paravirt) | Best performance |
| Firewall | ❌ Unchecked | Using pfSense for all firewall logic |
nano /etc/apt/sources.list
# Delete the line beginning with: deb cdrom:[Debian GNU/Linux...
Then install the QEMU guest agent and Docker (running as root, no sudo needed):
apt-get update && apt-get install -y qemu-guest-agent
systemctl enable --now qemu-guest-agent
curl -fsSL https://get.docker.com | sh
ip link set vmbr0 promisc on
This takes effect immediately but won't survive a reboot. To make it persistent:
nano /etc/network/interfaces
Add a post-up line to your vmbr0 block:
iface vmbr0 inet static
address ... # existing lines stay as-is
...
post-up ip link set vmbr0 promisc on # add this line
Verify it took effect:
ip link show vmbr0 | grep -i promisc
You should see PROMISC in the flags.
ip route | grep default
Output example: default via 192.168.1.1 dev eth0 proto dhcp. The interface name (eth0, ens18, enp2s0, etc.) is needed for the parent: field in the Compose file. Do not blindly use eth0 — names vary by OS and kernel version.
Also check your UID/GID for PUID/PGID fields:
id
mkdir ~/burner-browser && cd ~/burner-browser
nano compose.yaml
Paste the following, substituting your values:
services:
firefox:
image: lscr.io/linuxserver/firefox:latest
container_name: burner_browser
environment:
- PUID=1000 # Replace with your uid from `id`
- PGID=1000 # Replace with your gid from `id`
- TZ=Etc/UTC # Set to your timezone
networks:
macvlan_net:
ipv4_address: 192.168.1.50 # Must match BURNER_IP alias in pfSense
dns:
- 10.2.0.1 # ProtonVPN DNS from your .conf file
restart: unless-stopped
mem_limit: 2g
cpus: 2
networks:
macvlan_net:
driver: macvlan
driver_opts:
parent: eth0 # Replace with your interface from `ip route`
ipam:
config:
- subnet: 192.168.1.0/24
gateway: 192.168.1.1 # Your pfSense LAN IP
ports: section. With macvlan, the container has its own IP on the LAN. Port mapping is a bridge networking concept — it doesn't work with macvlan. Access the container directly at http://192.168.1.50:3000.docker compose up -d
docker compose logs -f
A healthy start shows the LinuxServer init completing with no errors. The Firefox UI takes 15–20 seconds to initialize on first boot.
Do not skip verification. A misconfigured kill switch or a DNS leak can silently expose you while appearing to work. Run every check below before trusting this environment with any sensitive session.
http://192.168.1.50:3000
You should see a full desktop Firefox browser running inside a noVNC web interface. If it doesn't load:
docker compose ps — is the container in "Up" state?192.168.1.50 from another LAN machine? If not, check Proxmox promiscuous mode.docker inspect burner_browser | grep IPAddress+address) — never your primary email address.cd ~/burner-browser
docker compose down
docker volume prune -f
On your next use, docker compose up -d spins up a clean, stateless instance. The pfSense rules remain in place permanently — only the container is ephemeral.
- ./config:/config under volumes:. Be aware this reintroduces some persistence — weigh the tradeoff for your threat model.By following these steps, you have successfully decoupled a specific browsing activity from your main network identity. The security chain, layer by layer:
Tear down the container after each session (docker compose down) to start with a clean slate.