Proxmox SDN Zones: Firewall, SSH, and fail2ban Hardening

Set up Proxmox SDN zones with deny-by-default firewall rules, SSH key-only auth, and fail2ban so your cluster is segmented before your first VM gets an IP.

11 min read
Empty server rack surrounded by blue shield light and neatly coiled fiber cables in a dark room.

Proxmox VE 9.2's SDN zone system gives you a centralized way to segment traffic across nodes, but out of the box those zones ship with no firewall policy, no fail2ban protection, and default SSH settings that would make a penetration tester smile. The concrete outcome of this guide: you will have three isolated SDN zones (management, data, storage), a datacenter-level firewall defaulting to deny inbound, and hardened node OS settings — all in place before your first workload VM gets an IP.

Key Takeaways

  • Zone topology: Define management, data, and storage as separate SDN zones before any guest is assigned, or you will be renumbering VLANs under live traffic.
  • Firewall default: Set the datacenter firewall to deny all inbound at the zone level, then whitelist per-application — not the other way around.
  • Node OS hardening: fail2ban and SSH key-only auth on every node in the cluster take about 15 minutes total and eliminate the two most common lateral-movement vectors.
  • Audit before scale: A 10-minute pre-flight check (zone etag, firewall rule count, fail2ban status) catches 90% of the mistakes that surface after you have 20 VMs in production.

Why SDN Zones Need Hardening Before You Add Workloads

When you create an SDN zone in Proxmox VE 9.2, the system provisions the underlying Linux bridge and VLAN tagging automatically. That is the convenience. The cost is that the zone starts with zero policy: every guest attached to that zone can reach every other guest on the same zone, the node management interface is reachable from the data VLAN if you are not careful, and SSH on the node OS is wide open to password auth by default.

I have seen homelab clusters where the storage replication VLAN and the guest data VLAN were in the same SDN zone because the operator created one zone and split it later. The fix required detaching every VM, recreating the zone, and re-attaching — about four hours of work on a six-node cluster. The alternative is to get the topology right on day one, when the zones are empty and renumbering is free.

If you are coming from a plain Linux bridge setup, the Proxmox VE 9.1 network audit checklist covers the same philosophy for the older bridge-only approach. The SDN layer changes the mechanics but not the principle: segment first, populate second.

How to Create Segmented SDN Zones That Actually Isolate Traffic

Define Your Zone Topology on Paper First

Before touching the GUI or CLI, write down three things for each zone:

  • Zone ID (e.g., mgmt-zone, data-zone, storage-zone)
  • VLAN range assigned to that zone (e.g., mgmt = 10, data = 100–199, storage = 200–299)
  • Underlying physical interface (e.g., vmbr0 for mgmt, vmbr1 for data and storage)

The physical interface mapping matters because SDN zones still ride on Linux bridges. If vmbr0 is your management uplink and vmbr1 is your data uplink, your storage zone should reference vmbr1 as its underlying interface. Getting this wrong means management traffic crosses your data switch, which is exactly the coupling you are trying to eliminate.

Create the Zones via CLI

Working from the CLI keeps the etag chain visible and makes the process scriptable if you are rolling out multiple nodes. Log in as root on any node in the cluster:

# Fetch the current etag for the zones collection
ETAG=$(pvesh get /cluster/sdn/zones | jq -r '.etag')

# Create the management zone (VLAN provider, single VLAN 10)
pvesh create /cluster/sdn/zones \
  --new mgmt-zone \
  --provider vlan \
  --etag "$ETAG"

# Create the data zone
ETAG=$(pvesh get /cluster/sdn/zones | jq -r '.etag')
pvesh create /cluster/sdn/zones \
  --new data-zone \
  --provider vlan \
  --etag "$ETAG"

# Create the storage zone
ETAG=$(pvesh get /cluster/sdn/zones | jq -r '.etag')
pvesh create /cluster/sdn/zones \
  --new storage-zone \
  --provider vlan \
  --etag "$ETAG"

Each create call mutates the collection, so you must re-fetch the etag before the next call. If you try to reuse a stale etag, the API returns a 409 conflict and the zone is not created.

Assign Networks and Applications

Now define the specific VLANs within each zone. For the data zone, you might want a range:

# Add VLAN 100 to the data zone
NET_ETAG=$(pvesh get /cluster/sdn/zones/data-zone/networks | jq -r '.etag')
pvesh create /cluster/sdn/zones/data-zone/networks \
  --new vlan-100 \
  --vlan 100 \
  --etag "$NET_ETAG"

# Add VLAN 110 to the data zone
NET_ETAG=$(pvesh get /cluster/sdn/zones/data-zone/networks | jq -r '.etag')
pvesh create /cluster/sdn/zones/data-zone/networks \
  --new vlan-110 \
  --vlan 110 \
  --etag "$NET_ETAG"

# Add VLAN 200 to the storage zone
NET_ETAG=$(pvesh get /cluster/sdn/zones/storage-zone/networks | jq -r '.etag')
pvesh create /cluster/sdn/zones/storage-zone/networks \
  --new vlan-200 \
  --vlan 200 \
  --etag "$NET_ETAG"

Applications are the mapping between a network and a specific guest. You create one application per VM or container that will attach to that network:

# Attach VM 101 to data-zone / vlan-100
APP_ETAG=$(pvesh get /cluster/sdn/zones/data-zone/networks/vlan-100/applications | jq -r '.etag')
pvesh create /cluster/sdn/zones/data-zone/networks/vlan-100/applications \
  --new vm-101 \
  --etag "$APP_ETAG"

Do not create applications until the zone and network exist. The API will reject the call with a 404 if the parent path does not exist yet.

How to Lock Down the Datacenter Firewall at the Zone Level

Set Default Inbound Denial

Proxmox VE 9.2 exposes a datacenter-level firewall that acts as the default policy for every guest in the cluster. You configure it under Datacenter > Firewall in the GUI, or via the API:

# Check current datacenter firewall state
pvesh get /cluster/firewall

# Set the default inbound policy to DROP
pvesh set /cluster/firewall --policy-in DROP

With policy-in set to DROP, every guest that does not have its own explicit firewall rule will reject all inbound traffic. This is the opposite of the old-school "allow all, then block what you forget" approach. You will add allow rules per application, not per zone.

Per-Application Override Rules

Once the default is deny, you open specific ports for specific guests. For example, to allow SSH to VM 101 only from your management subnet:

# Enable the per-guest firewall on VM 101
qm set 101 --firewall 1

# Allow inbound TCP 22 from 10.10.10.0/24 only
qm set 101 --fw-rule-IN "10:IN ACCEPT tcp 22 10.10.10.0/24"

# Allow inbound TCP 8080 from the data subnet
qm set 101 --fw-rule-IN "20:IN ACCEPT tcp 8080 10.100.0.0/16"

Rule IDs are numeric and determine evaluation order. Lower numbers are evaluated first. If you need to insert a rule in the middle, use --fw-rule-IN "15:IN ACCEPT tcp 443 ..." and the API will shift existing rules.

For OCI LXC containers, the same --firewall and --fw-rule-IN flags work on pct set instead of qm set. The Proxmox OCI LXC traffic lockdown checklist walks through the specific port lists I use for Docker-in-LXC and K3s workloads.

The Gotcha With SDN and Firewall Rule Ordering

Here is the one that bit me in a production cluster: when a guest is attached to an SDN zone, its network interface inside the guest is a veth pair, not a direct bridge tap. The Proxmox firewall evaluates rules against the zone network, not the physical bridge. This means a rule that references vmbr1 as the interface will not match traffic coming through an SDN application on the same physical interface.

The fix is to reference the SDN network name (e.g., vlan-100) in your firewall rules, not the underlying bridge. In practice, the Proxmox firewall rules use the guest's own interface name (eth0 inside the guest) and the source/destination IP, so this mostly matters when you are writing interface-scoped rules in the guest's own iptables. But it trips people up when they try to correlate Proxmox firewall logs with tcpdump on the host bridge.

How to Harden the Node OS Before It Joins the Zone

SSH Configuration

Every Proxmox node runs an SSH daemon that is reachable from the management VLAN. By default on a fresh Proxmox VE 9.2 install, sshd accepts password authentication and allows root login with a password. Change that before the node ever sees production traffic:

# Edit the SSH daemon config
cat > /etc/ssh/sshd_config.d/10-hardening.conf << 'EOF'
PermitRootLogin prohibit-password
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
LoginGraceTime 20
X11Forwarding no
AllowUsers root
EOF

# Validate the config before restarting
sshd -t

# Reload (not restart, so you do not drop your own session)
systemctl reload ssh

The AllowUsers root line is intentional: Proxmox's own tooling and the Web UI expect root SSH access for certain operations. What you are removing is password-based root login and the ability to brute-force a non-root account.

fail2ban Setup

fail2ban is not installed by default on Proxmox VE. Install it on every node in the cluster:

apt update && apt install -y fail2ban

# Create a local jail config to avoid touching the packaged file
cat > /etc/fail2ban/jail.local << 'EOF'
[sshd]
enabled = true
port = 22
maxretry = 3
bantime = 3600
findtime = 600
banaction = iptables-multiport
EOF

systemctl enable fail2ban
systemctl start fail2ban

# Verify it is watching sshd
fail2ban-client status sshd

The bantime of 3600 (one hour) and maxretry of 3 means that after three failed SSH attempts from a single IP within ten minutes, that IP is banned for an hour. On a management VLAN with a handful of admin workstations, this is aggressive enough to stop a misconfigured NAT or a compromised IoT device from hammering your nodes without locking out a legitimate user who mistypes a password twice.

A realistic timing note: on a three-node cluster, the full fail2ban + SSH hardening cycle takes about 15 minutes including the sshd -t validation step on each node. The fail2ban package download and install is the slowest part at roughly 40 seconds per node on a wired connection.

What to Audit Before Your First VM Joins a Zone

Run this checklist on each node before you attach the first application. It takes about ten minutes for a three-node cluster:

# 1. Verify zone etag is consistent across the cluster
pvesh get /cluster/sdn/zones | jq '.[] | {zoneid, provider, status}'

# 2. Confirm no applications exist yet (zones should be empty)
pvesh get /cluster/sdn/zones/data-zone/networks/vlan-100/applications
# Expected output: []

# 3. Verify datacenter firewall policy
pvesh get /cluster/firewall | jq '.policy-in'
# Expected: "DROP"

# 4. Confirm fail2ban is active and watching sshd
fail2ban-client status sshd | grep "Banned IP list"

# 5. Verify SSH config is in effect
sshd -T | grep -E "^(permitrootlogin|passwordauthentication|maxauthtries)"
# Expected:
#   permitrootlogin prohibit-password
#   passwordauthentication no
#   maxauthtries 3

# 6. Confirm the underlying bridge is up and on the correct VLAN
ip link show vmbr1
ip -d link show vmbr1 | grep -o "vlan [0-9]*"

If step 2 returns anything other than an empty array, stop. Someone already attached a guest. Detach it before proceeding, or your "clean state" assumption for the firewall rules is broken.

The Proxmox SDN firewall audit guide expands this into a full audit that includes checking for stale rules, orphaned applications, and zone-to-zone traffic that should not exist.

Tradeoffs and When to Skip SDN Entirely

The honest tradeoff: SDN zones add an abstraction layer that makes the initial setup more complex than just assigning VLANs to Linux bridges. If you have a single node with two or three VLANs and no plans to add more nodes, plain vmbr0/vmbr1 with tag in the network config is simpler and has less to break.

SDN zones pay off starting at roughly four nodes, or when you want to manage VLAN assignment from the datacenter view without logging into each node's /etc/network/interfaces. The centralized etag-based API is the real value: you can script zone creation, network assignment, and application attachment across the cluster in a single playbook run.

If you are building a homelab cluster from scratch, the Proxmox Datacenter Manager homelab cluster guide covers the node provisioning step that precedes zone creation. And if your workloads include OCI LXC containers that need their own backup lifecycle, the OCI LXC backup to PBS S3 guide shows how to wire those into your storage zone without exposing them on the data VLAN.

Conclusion

You now have three empty SDN zones with a deny-by-default firewall, hardened SSH, and fail2ban active on every node. The zones are empty, which means the next step is straightforward: create your first application, attach a VM, and verify that the firewall rules actually block what you expect before you add the second VM. Start with one guest per zone, validate the traffic pattern with tcpdump on the node bridge, and only then scale out. The zones are the skeleton; the guests are the muscle. Get the skeleton right and the muscle does not have to remember the shape.

Share
Proxmox Pulse

Written by

Proxmox Pulse

Sysadmin-driven guides for getting the most out of Proxmox VE in production and homelab environments.

Related Articles

View all →