Writeup | Networking & Infrastructure

Enterprise VLAN Segmentation Migration

Taking a flat homelab network from every device on a single broadcast domain to a fully segmented 10-VLAN production design — with a hardware swap mid-migration that broke 25+ services, a Proxmox cluster quorum failure, and 30+ services needing network and config updates before anything came back online.

OPNsense Cisco SG300 Netgear GS728TP 802.1Q VLANs Proxmox Firewall Design DNS Migration Troubleshooting

STAR Breakdown

  • Situation: The entire homelab ran on a flat network — Proxmox nodes, game servers, SIEM, storage, IoT devices, and DMZ services all on the same broadcast domain with no traffic isolation. A hardware swap from legacy Cisco 3750V2 switches to a Cisco SG300-20 + Netgear GS728TP immediately took down 25+ services and left the Proxmox cluster without quorum.
  • Task: Design a production VLAN architecture, restore connectivity after the switch swap, migrate every service to its correct VLAN and addressing plan, enforce per-VLAN firewall rules in OPNsense, and update DNS, NAT, NPMplus, and application configs to point to the new locations.
  • Action: Designed a 10-VLAN segmentation scheme, restored Proxmox corosync quorum via SG300 trunk configuration, migrated 30+ services with DHCP reservations, rewrote DNS host overrides, updated NAT policy, reconfigured NPMplus proxy backends, migrated storage to a dedicated storage VLAN, and deployed explicit OPNsense firewall rules.
  • Result: Fully segmented production network — 10 active VLANs, Proxmox cluster healthy (3/3 quorum), all services reachable, firewall default-deny enforced, and zero unnecessary cross-VLAN exposure.

Where It Started

When the homelab was first built, everything lived on one flat private subnet. That's fine when you're getting started — one switch, one subnet, one gateway. But at some point the lab had grown to 22 VMs and LXC containers plus physical switches, NAS units, APs, and OOB management cards all on the same network. A guest device on the WiFi could technically reach the Proxmox management API. The Minecraft game server was on the same broadcast domain as the Wazuh SIEM and the Synology NAS. None of that is acceptable in a network that's supposed to model enterprise security practices.

The plan: redesign the network around 10 VLANs, each with a purpose, each enforced at the OPNsense firewall with explicit rules. The addressing scheme uses a consistent private pattern for gateways, static infrastructure, service reservations, DHCP clients, and spare capacity. The exact subnet values stay private, but the operational goal was predictable addressing with clear firewall boundaries.


The VLAN Design

The 10-VLAN production layout, each with a specific security posture:

Public-safe VLAN migration diagram

The key security decisions built into the design:


Phase 1 — The Switch Swap That Broke Everything

The legacy switching was a pair of Cisco Catalyst 3750V2 switches — 100 Mbps FastEthernet to everything. Both were replaced with a Cisco SG300-20 core switch and a Netgear GS728TP PoE access switch, both gigabit. The physical swap was quick. The aftermath wasn't.

The new switches came up with default configs. The legacy cloudflared DMZ VLAN wasn't trunked anywhere. The Proxmox nodes were plugged into the SG300 but the management VLAN wasn't configured on those ports yet. Every service that depended on the network came down immediately.

Restoring cloudflared first

The first priority was getting public ingress back. cloudflared was on the legacy DMZ VLAN, but that VLAN wasn't tagged on the required SG300 ports. Fix: add the DMZ VLAN to the SG300 VLAN database, tag it on the OPNsense and Proxmox-facing trunks, confirm the LXC gets its DHCP lease, and verify the Cloudflare tunnel registers:

! Cisco SG300 — restore legacy DMZ path for cloudflared
vlan database
 vlan [legacy-dmz-vlan]
 exit
interface [proxmox-facing-trunk]
 switchport trunk allowed vlan add [legacy-dmz-vlan]
interface [opnsense-facing-trunk]
 switchport trunk allowed vlan add [legacy-dmz-vlan]
copy running-config startup-config

After applying: cloudflared could reach its DMZ gateway, the Cloudflare tunnel registered, and public services came back online.

Proxmox quorum failure

The bigger problem: Proxmox corosync had already been configured to use the management VLAN addresses (static management addresses for the three Proxmox nodes) but the management VLAN wasn't tagged on the SG300 ports facing nodes 2 and 3 — only one node trunk had it. Corosync uses multicast on a shared VLAN to maintain quorum. Without management VLAN reachability, the cluster showed 1/3 nodes and all cluster operations were blocked.

The fix required identifying exactly which physical switch ports connected to each Proxmox node using CDP/LLDP, then adding the management VLAN tag to those ports:

! Confirm ports via LLDP
show lldp neighbors detail

! Public-safe labels: Proxmox trunks and OPNsense trunk
interface range [proxmox-node-trunks]
 switchport trunk allowed vlan add [management-vlan]
interface [opnsense-facing-trunk]
 switchport trunk allowed vlan add [management-vlan]
copy running-config startup-config

! Verify quorum restored
pvecm status
# Quorum information
# Node votes: 3
# Expected votes: 3
# Total votes: 3
# Quorum: YES

Phase 2 — VLAN Cutover: 30+ Services

With the cluster healthy and public services restored on the legacy addresses, the actual VLAN migration could begin. Every service needed to move from its old flat-network address to its new role-based VLAN address. This wasn't just changing addresses — every dependency had to be updated at the same time or services would break the moment the network location changed.

The dependency chain

For each service the update chain was:

  1. Set DHCP reservation in OPNsense with the new VLAN address
  2. Update the LXC/VM network config in Proxmox (VLAN tag on the bridge interface)
  3. Reboot or reconfigure the container to pick up the new address
  4. Update OPNsense Unbound DNS host override to point the subdomain to the new service location
  5. Update NPMplus proxy host backend to the new service location
  6. Update any service-to-service configs (Authentik URLs, Jellyfin NFS mounts, etc.)
  7. Verify via firewall logs that the rule allowing that traffic existed

Doing all 30+ services by hand through the OPNsense and Proxmox UIs would have taken days and been error-prone. Instead, the DNS overrides and NPMplus backends were updated in bulk via the OPNsense API:

# Update Unbound DNS host overrides via OPNsense API
# Each service subdomain gets a sanitized private target

curl -sk -u "$KEY:$SECRET" -X POST \
  https://[opnsense-mgmt-ip]/api/unbound/settings/addHostOverride \
  -H "Content-Type: application/json" \
  -d '{"host":{"enabled":"1","hostname":"auth","domain":"masternazz.com",
       "rr":"A","server":"[sanitized-service-target]","description":"Authentik SSO"}}'

# Repeat for approved service subdomains, then apply:
curl -sk -u "$KEY:$SECRET" -X POST \
  https://[opnsense-mgmt-ip]/api/unbound/service/reconfigure

Services that needed special handling

Most LXC containers were straightforward — change VLAN tag, reboot, pick up the new address, update DNS. A few required more work:

Authentik: OPNsense had Authentik configured as both an LDAP auth server and an OIDC provider. Both configs had hardcoded service addresses. Updated via the diag_backup.php config restore mechanism (since direct writes to /conf/config.xml require root access that the API account doesn't have).

Jellyfin media stack: The NFS mount from Synology was still pointed at the old flat-network address. After Synology moved to the storage VLAN, the NFS export had to be re-added, and the Proxmox storage definition updated. The Docker bind mounts inside the LXC then picked up the correct path.

Proxmox Backup Server: The Thecus NAS running PBS needed its network interface reconfigured from the legacy flat subnet to a static address on the storage VLAN, the SG300 port set to the storage access VLAN, and the PBS storage target in Proxmox updated to the new private address.

Wazuh SIEM: Every other VLAN needed firewall rules allowing outbound to Wazuh for agent registration and log forwarding. These rules were added per-interface so each VLAN could reach Wazuh without having general inter-VLAN access.

Windows Server 2025: Moved to the server VLAN and the Wazuh agent was installed via WinRM to start forwarding Windows event logs into the SIEM.


Phase 3 — Firewall Rules

OPNsense uses a default-deny policy — if there's no rule allowing traffic, it's blocked. Every VLAN needed a minimal set of rules:

The most important rules are the inter-VLAN paths for DMZ egress. NPMplus in the DMZ needs to reach only the approved backend groups:

! Public-safe OPNsense firewall rule model — DMZ egress
# NPMplus -> approved internal-service backends only
allow  src=[reverse-proxy-host]  dst=[approved-service-backends]

# NPMplus -> approved game-server backends only
allow  src=[reverse-proxy-host]  dst=[approved-game-backends]

# NPMplus -> approved management web backends only
allow  src=[reverse-proxy-host]  dst=[approved-management-web-backends]

# cloudflared can ONLY talk to NPMplus — nothing else in DMZ
allow  src=[tunnel-host]  dst=[reverse-proxy-host]

The DMZ implicit deny catches anything NPMplus tries to reach that wasn't explicitly whitelisted. If a new backend service is added, it needs a matching firewall rule before NPMplus can proxy to it.


Blockers Hit Along the Way

Proxmox routing conflict

After the management VLAN was configured, some connections to legacy flat-network devices (Cisco switch, AP) started failing from Proxmox. Root cause: a stale manual IP in the legacy flat-network range had been added to the management VLAN bridge interface) during earlier testing. This caused Proxmox to route all traffic destined for legacy management devices out the management VLAN interface instead of the native bridge, where the physical legacy devices actually were. Fix: remove the stale address from the management VLAN interface.

Synology NAS — didn't survive port move

Moving the Synology NAS switch port from the legacy network to the storage VLAN required the DSM network settings to already be configured with the target storage VLAN address before the port move — otherwise the NAS goes silent the moment the port VLAN changes. The lesson: update the device address first (while still reachable on the legacy network), then move the switch port. The Synology migration required logging into DSM while still on the legacy network, configuring the static address for the storage VLAN, and only then moving the SG300 port.

NPMplus backend updates

NPMplus stores proxy host configs as nginx conf files on disk. When every service address changed, all 18+ proxy host backends needed updating. Rather than editing each one through the NPMplus UI, the nginx configs were patched via sed and nginx reloaded — but NPMplus regenerates configs on changes, so the correct approach was updating through the NPMplus API or UI so the database stayed in sync with disk.

VLAN 1 couldn't be retired immediately

Even after most services migrated, VLAN 1 stayed live because of out-of-band management hardware — the IBM M4 IMM2 (IPMI/remote console for firstmoon) was still on the legacy flat network and needed physical access to reconfigure. Legacy VLAN 1 remains as a migration zone until OOB cards are moved to the management VLAN.


The End State

From start to finish this took roughly two weeks of iterative work across multiple sessions. The final production state:


Key Takeaways

Map dependencies before cutting over. Every service has at least 3–5 things that need updating when its network location changes: DHCP reservation, DNS, reverse proxy backend, any application configs that reference it by address, and firewall rules. Missing one keeps the service broken even after the addressing is right.

Bulk API updates beat the UI. Updating 25 DNS overrides and 18 proxy backends one at a time through a web UI takes hours and introduces typos. OPNsense and NPMplus both have APIs. Writing a script that applies all changes atomically and can be re-run is faster and leaves a record of what changed.

Hardware swaps during a migration are risky. Replacing the switches mid-migration added a full extra phase of work — re-trunking all VLANs, recovering quorum, restoring the DMZ path for cloudflared. If the switch hardware had been swapped after the VLAN design was validated on the old hardware, the transition would have been cleaner. Order matters.

Device-by-device NAS migrations. Network devices that don't have a console (switches excepted) need their address changed in software before the switch port moves. The Synology NAS taught this lesson the hard way — configure the target address in DSM first, then move the port.

Default-deny firewall forces you to document traffic flows. Every path that needs to work has to become an explicit rule. That's annoying during setup but it means you always know exactly which services can talk to which — no accidental cross-VLAN bleed.

Related Pages