The Breakdown
I’ve been running untrusted workloads on my Proxmox VMs for a while, but I never felt comfortable with the blast radius. My initial setup used vmbr0 (bridged to my main LAN), which meant if a workload found a way to pivot or escape, it had a direct path to my infrastructure.
I needed a “fishbowl” environment. I wanted a VM that could “see” the internet (to trigger the workload’s behavior) but couldn’t actually talk to anything outside the host.
The solution wasn’t just a VLAN; it was a combination of a dedicated virtual bridge (vmbr1), strict iptables rules on the host, and a switch to the Serial Console (xterm.js) for management. This setup creates a network that looks connected but is functionally air-gapped from everything except the host’s DNS resolver.
The Solution
Create the Isolated Bridge (vmbr1)
First, I created a new bridge in Proxmox that has no physical ports. This ensures no traffic can accidentally slip through to nic0.
- Name:
vmbr1 - Bridge ports:
(Empty) - IP Address:
172.16.16.16/24 - Gateway:
(Empty)
Configure the Analysis VM
When creating the VM, select vmbr1 as the network interface. Since there is no DHCP server on this bridge, you must assign a Static IP inside the VM.
- VM IP:
172.16.16.100/24(Example) - Gateway:
(None)— There is no gateway for an isolated sandbox. - DNS:
172.16.16.16— This points to the Proxmox host, which ouriptablesrules allow.
Netplan Configuration:
network:
version: 2
renderer: networkd
ethernets:
ens18:
dhcp4: no
addresses:
- 172.16.16.100/24
nameservers:
addresses:
- 172.16.16.16/etc/netplan/00-installer-config.yaml
The “Fishbowl” Rules (Host Level)
This is the critical part. Because the VM is bridged to the host, traffic destined for the Proxmox IP doesn’t go through the FORWARD chain - it goes through the INPUT chain.
Run this script on the Proxmox host before starting the VM. It allows DNS queries but drops everything else destined for the host.
#!/bin/bash
# Enable IP forwarding (required for bridged traffic)
sysctl -w net.ipv4.ip_forward=1
VM_SUBNET="172.16.16.0/24"
# 1. Allow DNS queries to the Host (UDP & TCP)
# This allows the VM to resolve domains via the host's resolver
iptables -A INPUT -i vmbr1 -s $VM_SUBNET -p udp --dport 53 -j ACCEPT
iptables -A INPUT -i vmbr1 -s $VM_SUBNET -p tcp --dport 53 -j ACCEPT
# 2. Allow Established/Related traffic (so DNS replies return to the VM)
iptables -A INPUT -i vmbr1 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# 3. DROP ALL OTHER TRAFFIC DESTINED FOR THE PROXMOX HOST
# This prevents the VM from pinging the host, accessing SSH, or SMB shares
iptables -A INPUT -i vmbr1 -s $VM_SUBNET -j DROPisolate_vmbr1.sh
Capture and Analyze
Since the VM has no real internet access, any traffic leaving vmbr1 is either a DNS query or a failed connection attempt. This is perfect for telemetry.
Run this on the Proxmox host:
# Capture all traffic on the isolated bridge
sudo tcpdump -i vmbr1 -w untrusted_traffic_capture.pcap -s 0
# Analyze TLS Server Name Indication (SNI)
tshark -r untrusted_traffic_capture.pcap -Y "tls.handshake.extensions_server_name" \
-T fields -e tls.handshake.extensions_server_name
Note on NAT: If you use Docker inside the VM, the source IP in your PCAP will be the Linux VM’s IP (172.16.16.100), not the container’s IP, because Docker performs NAT on the bridge. For SNI analysis, capturing on vmbr1 is sufficient.
Management via Serial Console (xterm.js)
Because the network is isolated, I no longer use SSH or the traditional VNC console. Instead, I use the Serial Console in the Proxmox web interface.
Temporary Internet for Updates
To keep the VM updated without rebuilding the network, you can temporarily toggle the isolation:
- Allow Egress:
iptables -I FORWARD -s 172.16.16.0/24 -j ACCEPT - Enable NAT:
iptables -t nat -A POSTROUTING -s 172.16.16.0/24 -j MASQUERADE - Update VM:
- Re-Isolate: Remove the
FORWARDrule, remove theNATrule, and run theisolate_vmbr1.shscript again.
Docker Containers (Optional)
If you plan to run multiple untrusted workloads in the same session, Docker adds a layer of isolation.
# Inside the VM, create an isolated Docker network
docker network create \
--driver bridge \
--subnet=172.20.0.0/16 \
--gateway=172.20.0.1 \
untrusted-bridge
# Run the workload with strict capabilities
docker run -d \
--name workload-sandbox \
--network=untrusted-bridge \
--cap-drop=ALL \
--cap-add=NET_RAW \
--read-only \
--tmpfs /tmp \
--security-opt=no-new-privileges \
--rm \
<your-workload-image>
Isolation is only as good as your last rule. By combining a dedicated bridge with iptables on the INPUT chain, I turned a standard VM into a secure sandbox. Using the Serial Console for management ensures that the “fishbowl” remains sealed while I’m inside it. Next time, I’ll add a local dnsmasq instance to the host to sinkhole known-bad domains before they even reach the VM.