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.

⚠️
Before you begin: Know your LAN subnet, your Proxmox host's IP, and which NIC pfSense uses for your LAN interface. The examples throughout use 192.168.1.0/24 — substitute your own subnet wherever you see it.
Phase 1 — Establish the ProtonVPN Gateway in pfSense

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.

Create the Burner IP Alias Do this first — every firewall rule, NAT rule, and policy route throughout this guide references this alias by name. Navigate to Firewall → Aliases and click + Add:
FieldValueNotes
NameBURNER_IPNo spaces — this exact name is referenced in every rule that follows
TypeHost(s)Not "Network" — you're aliasing a single host
IP or FQDN192.168.1.50An unused static IP on your LAN. Must match the IP you assign to the Docker container in Phase 4.
DescriptionBurner browser container IP
Save. Aliases apply immediately — no separate Apply step needed.
💡
Verify 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.
Get the WireGuard Config from ProtonVPN Log into account.proton.meDownloads → WireGuard configuration. Configure as: Platform: Router, Protocol: WireGuard. Download the .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.
Build the WireGuard Tunnel in pfSense Navigate to VPN → WireGuard → Tunnels+ Add Tunnel:
pfSense FieldValueSource
DescriptionProtonVPNYour choice
Listen Port51820Standard WireGuard port
Private KeyPaste from [Interface] PrivateKeyYour .conf file
Interface Address10.2.0.2/32[Interface] Address
Save, then click the Peers tab and add a peer:
pfSense FieldValueSource
TunnelSelect the tunnel you just created
Public KeyPaste from [Peer] PublicKeyYour .conf file
Pre-shared KeyLeave blankProtonVPN doesn't use PSKs by default
Allowed IPs0.0.0.0/0Routes all traffic through the tunnel
EndpointIP portion of [Peer] EndpointYour .conf file
Endpoint Port51820Port portion of Endpoint
Keep Alive25Maintains tunnel through NAT
⚠️
Do not enable "Dynamic Endpoint" — ProtonVPN servers have static IPs. Leaving it enabled can cause the tunnel to drop.
Assign and Enable the WireGuard Interface Navigate to Interfaces → Assignments. Find tun_wg0 in the dropdown and click + Add. Click the new interface (OPT1 or similar) to edit:
FieldValueWhy
Enable✅ CheckedDisabled by default
DescriptionPROTON_VPNReferenced by name in every rule
IPv4 Configuration TypeStatic IPv4Must be Static — "None" prevents gateway creation
IPv4 AddressIP from [Interface] Address (e.g. 10.2.0.2)Tunnel's local endpoint
Subnet mask32ProtonVPN assigns a /32 point-to-point address
IPv6 Configuration TypeNoneIPv6 on WireGuard requires separate config
Block Private Networks❌ UncheckedVPN peer uses RFC1918 — blocking breaks the tunnel
MTU1420Standard for WireGuard over UDP
Save and Apply Changes.
Configure Outbound NAT Navigate to Firewall → NAT → Outbound. Change mode from Automatic to Hybrid Outbound NAT and Save.
💡
Why Hybrid, not Manual? Hybrid preserves automatic rules that keep normal LAN traffic working, while letting you add custom rules on top.
Click + Add to create a mapping at the top of the list:
FieldValueNotes
InterfacePROTON_VPNThe interface you just created
Protocolany
SourceSingle host or alias → BURNER_IPUse the alias — pfSense resolves at evaluation time
Destinationany
Translation → AddressInterface AddressNATs the burner traffic using the WireGuard tunnel's IP
Static Port❌ UncheckedOnly needed for VoIP/gaming
DescriptionBurner → ProtonVPN
Save and Apply. This rule must appear above any existing automatic rules for the same source range.
Create the Gateway & Verify Tunnel is Up Navigate to System → Routing → Gateways. pfSense may have auto-created a gateway. If so, edit it to verify these settings — otherwise click + Add:
FieldValueNotes
InterfacePROTON_VPN
NamePROTON_VPN_GWExact name referenced in the policy routing rule
Gateway IP10.2.0.1DNS value from [Interface] — the far end of the WireGuard tunnel. Not the public Endpoint IP.
Monitor IP10.2.0.1pfSense pings this to determine tunnel health
State Killing on Gateway FailureKill states for down gatewaysForces pfSense to drop all connections when VPN goes down — not let them linger on WAN
Use non-local gateway (Advanced)Check thisRequired with /32 interface address — pfSense would otherwise reject 10.2.0.1 as outside the interface subnet
After saving, go to Status → WireGuard. The peer should show a "Last Handshake" within the last 2 minutes and rising "Transfer" byte count.
Phase 2 — DNS Leak Prevention

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.

Find Your VPN's DNS Server IP Open your ProtonVPN .conf file. Look for the DNS = line in the [Interface] block:
[Interface]
...
DNS = 10.2.0.1   ← this is the value you need
⚠️
This IP varies by server and region. 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.
Note this IP: 10.2.0.1 . You will use it in the Docker Compose file and the NAT rule below.
Create a DNS Hijack Rule via NAT Port Forward This rule intercepts any DNS query sent from your burner IP — including hard-coded DNS servers inside the container — and redirects it to the ProtonVPN DNS resolver. Navigate to Firewall → NAT → Port Forward+ Add:
⚠️
The Source field is hidden by default. You must click the Advanced button on the Source row to expand it. If you skip this, the rule intercepts DNS from your entire LAN, breaking DNS for every device on the network.
FieldValueNotes
InterfaceLANThe interface your Proxmox host is on
ProtocolTCP/UDPDNS uses UDP 53 primarily, TCP 53 for large responses
Source (click Advanced)Single host → BURNER_IPOnly redirect DNS from this specific host
DestinationanyIntercepts regardless of which DNS server the container tries to reach
Destination Port53Standard DNS port
Redirect Target IP10.2.0.1Your VPN DNS server from above
Redirect Target Port53
NAT ReflectionDisableMust be explicitly disabled with "Any" destination — leaving it on may cause a save error
Filter rule associationAdd associated filter ruleAuto-creates the firewall PASS rule for the redirected traffic
💡
After saving, pfSense automatically adds a matching PASS entry in Firewall → Rules → LAN. No manual firewall rule needed.
Phase 3 — Policy Routing

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.

Create the Policy Routing Firewall Rule Navigate to Firewall → Rules → LAN. Click + Add at the top of the list. pfSense rules are first-match wins — if a more general "allow LAN to any" rule fires first, the gateway override is ignored.
FieldValueNotes
ActionPass
InterfaceLAN
Protocolany
SourceSingle host → BURNER_IPUse the alias
Destinationany
DescriptionRoute Burner via ProtonVPN
Before saving, expand Advanced Options:
Advanced FieldValueWhy
GatewayPROTON_VPN_GWThis is the policy routing override — the core of this entire setup. Without it, traffic routes via WAN.
Disable reply-toCheck thisPrevents pfSense from sending return traffic back out the wrong interface — asymmetric routing breaks TCP connections
Save and Apply. Drag this rule to the very top of the LAN list if it wasn't added there by default.
⚠️
State table caveat: Existing TCP connections from the burner IP may continue using the old WAN route until their state expires. After adding the rule, restart the container to force new connections.
Add the Kill Switch Block Rule With the Pass rule in place, immediately add the Block rule directly below it. If the VPN tunnel goes down, pfSense falls through to this rule and drops all burner traffic instead of letting it leak out your real WAN. In Firewall → Rules → LAN, click + Add and position it directly below the Pass rule:
FieldValueNotes
ActionBlockSilently drops packets — Reject would send a TCP RST back to the container
InterfaceLAN
Protocolany
SourceSingle host → BURNER_IPSame source as the Pass rule above
Destinationany
DescriptionKill Switch — block burner if VPN down
⚠️
Rule order is everything. LAN rules must read: (1) Pass rule with VPN gateway, (2) this Block rule directly below. If Block is above Pass, all burner traffic drops regardless of VPN state.
Save and Apply Changes.
Phase 4 — Spin Up the Isolated Docker Browser

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.

Create a Dedicated VM in Proxmox In the Proxmox web UI, click Create VM and work through the wizard: General tab:
FieldValueNotes
Nameburner-browser
OS tab:
FieldValueNotes
ISO ImageDebian 12 or Ubuntu 24.04 LTSMinimal/server install — no desktop environment needed
Guest OS TypeLinux
System tab:
FieldValueNotes
Machineq35Preferred for modern VMs
Qemu Agent✅ EnabledInstall qemu-guest-agent inside the VM after OS install
Disks tab:
FieldValueNotes
Disk size10 GiBSufficient for OS, Docker, and the Firefox image
Discard✅ EnabledEnables TRIM if storage is SSD/NVMe-backed
CPU tab:
FieldValueNotes
Cores2Sufficient for a single browser container
TypehostBest performance — passes through host CPU flags
Memory tab:
FieldValueNotes
Memory2048 MiBFirefox under noVNC uses ~1–1.5 GB under load
Network tab:
FieldValueNotes
Bridgevmbr0Same bridge your LAN devices are on
ModelVirtIO (paravirt)Best performance
Firewall❌ UncheckedUsing pfSense for all firewall logic
After OS install, remove the stale CD-ROM apt source first (Debian adds the ISO as a package source by default):
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
Enable Promiscuous Mode on the Proxmox Bridge macvlan requires promiscuous mode so the Proxmox bridge passes all LAN frames to the VM's NIC. Without it, the container's macvlan packets are silently dropped at the bridge before reaching pfSense. The Promiscuous Mode option was removed from the Proxmox web UI. SSH into your Proxmox host and run:
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.
⚠️
macvlan host isolation: the container cannot communicate back to the VM host directly — this is a kernel-level restriction of macvlan, not a configuration error. Access the container from any other machine on your LAN.
Find Your Host's Network Interface Name SSH into your Docker host:
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
Create the Docker Compose File
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
⚠️
Do not add a 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.
Start the Container
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.
Phase 5 — Verification & Execution

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.

Access the Containerized Browser From any machine on your LAN (not the Docker host itself, due to macvlan isolation), navigate to:
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?
  • Can you ping 192.168.1.50 from another LAN machine? If not, check Proxmox promiscuous mode.
  • docker inspect burner_browser | grep IPAddress
Verify VPN IP and DNS (No Leaks) Inside the containerized Firefox, navigate to ipleak.net. Verify:
  • Your IP address shows a ProtonVPN endpoint address, not your real ISP IP.
  • DNS Addresses shows ProtonVPN-owned servers only. If you see your ISP's servers, you have a DNS leak — revisit Phase 2.
  • No WebRTC leak — should show nothing or the same VPN IP.
⚠️
If ipleak.net shows your real IP, the policy routing rule is not matching. Check: (1) Is the rule at the very top of the LAN rules list? (2) Is the gateway set in Advanced Options? (3) Does the Source exactly match your BURNER_IP? Try Diagnostics → States → Reset States to force new connections.
Test the Kill Switch
  1. In the containerized browser, keep a page loading (e.g., ipleak.net refreshing).
  2. In pfSense, go to VPN → WireGuard → Tunnels and click Disable on your ProtonVPN tunnel.
  3. Within seconds, all internet access from the container should stop. The browser should show connection errors — not fall back to your real IP.
  4. Re-enable the tunnel. Traffic should resume once the WireGuard handshake re-establishes (~30 seconds).
💡
If traffic falls back to your real WAN instead of dropping, the kill switch Block rule is not in place or is in the wrong position. Re-check rule order in Phase 3.
Execute Your Session Your environment is verified. Proceed with your session:
  • Use an email alias service (SimpleLogin, AnonAddy, or a Gmail +address) — never your primary email address.
  • Avoid logging into any account you use on your real browser.
  • Treat the container as fully disposable — anything you do inside it should be assumable by an adversary who sees the VPN exit node's traffic.
Teardown After Use When your session is complete, destroy the container to clear all cookies, cached data, and session storage:
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.
💡
For a persistent home directory between sessions (e.g., to save bookmarks), add a named volume mount: - ./config:/config under volumes:. Be aware this reintroduces some persistence — weigh the tradeoff for your threat model.

Conclusion

By following these steps, you have successfully decoupled a specific browsing activity from your main network identity. The security chain, layer by layer:

  1. pfSense policy rule forces the burner IP through the VPN gateway — no manual VPN client needed.
  2. Outbound NAT correctly translates the burner's traffic so ProtonVPN accepts it.
  3. DNS hijack rule ensures even hard-coded DNS servers inside the container are redirected to the VPN resolver.
  4. Kill switch Block rule guarantees traffic drops entirely if the VPN tunnel goes down — no fallback to your real ISP.
  5. macvlan container provides a separate MAC address, ephemeral storage, and full process isolation from your host.

Tear down the container after each session (docker compose down) to start with a clean slate.