Most home labs start flat. One subnet, a consumer router, and a handful of services that someone port-forwarded during a late-night troubleshooting session. Every forwarded port is a door open to the internet, and every device on the same subnet can reach every other device. This guide shows you how to split the network into VLANs, place a default-deny firewall between the segments, and make a WireGuard tunnel the only path into your services from outside. It is written for self-hosters and tinkerers who are comfortable with a Linux shell and have a basic sense of how IP routing works.

Before you start

  • A Linux machine to act as the router, with two network interfaces: one for the WAN (ISP) side and one for the LAN trunk. The examples assume Debian or Ubuntu, with netplan and nftables.
  • A managed switch that supports 802.1Q VLAN tagging, so you can configure a tagged trunk port to the router and untagged access ports for devices.
  • Local console access to the router (monitor and keyboard, IPMI, or serial). Firewall and network changes can lock you out of SSH, so plan for that.
  • Your ISP modem or gateway in bridge mode, or at least with no port forwards configured, so the Linux router receives the WAN address directly.
  • A dynamic DNS hostname such as YOUR_DOMAIN if your ISP does not give you a static public IP.
  • Root or sudo access on the router and a WireGuard-capable client device for testing.

Step 1: Plan the address space and the segments

Write the plan down before touching any configuration. Four segments are enough for most labs:

  • VLAN 10, management (10.10.10.0/24): hypervisors, the switch management interface, and router administration.
  • VLAN 20, services (10.20.0.0/24): the applications you host, such as a wiki, a media server, or a password manager.
  • VLAN 30, trusted (10.30.0.0/24): your laptops, desktops, and phones.
  • VLAN 40, IoT (10.40.0.0/24): cameras, smart plugs, TVs, and other devices you do not fully trust.

Each segment uses the router as its default gateway at the .1 address. The policy you are building is simple: IoT devices can reach the internet but nothing internal, services can reach the internet for updates but cannot start connections into the trusted segment, and trusted clients reach only the service ports they need.

Step 2: Create the VLANs on the switch and the router

On the switch, create VLANs 10, 20, 30, and 40. Configure the port connected to the router as a tagged trunk that carries all four VLANs. Configure each device port as an untagged access port in the VLAN it belongs to. The menus differ by vendor, but the concept is the same everywhere.

On the router, define the VLAN subinterfaces in netplan. Save this as /etc/netplan/01-router.yaml, replacing the interface names with the ones shown by ip -br link:

network:
  version: 2
  ethernets:
    enp1s0:
      dhcp4: true
    enp2s0:
      dhcp4: false
  vlans:
    vlan10:
      id: 10
      link: enp2s0
      addresses: [10.10.10.1/24]
    vlan20:
      id: 20
      link: enp2s0
      addresses: [10.20.0.1/24]
    vlan30:
      id: 30
      link: enp2s0
      addresses: [10.30.0.1/24]
    vlan40:
      id: 40
      link: enp2s0
      addresses: [10.40.0.1/24]

Apply it with sudo netplan try. This command rolls the change back automatically if you do not confirm it within the timeout, which protects you from a bad remote change. Then confirm the interfaces came up:

ip -br addr show

Each vlanNN interface should show its gateway address.

Step 3: Enable forwarding and set a default-deny firewall

First, enable IPv4 forwarding and make it persistent:

echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-router.conf
sudo sysctl --system

Next, write the firewall. The important part is that the input and forward chains both default to drop. Only traffic you explicitly allow gets through. Save this as /etc/nftables.conf:

#!/usr/sbin/nft -f

flush ruleset

table inet filter {
  chain input {
    type filter hook input priority 0; policy drop;

    iif "lo" accept
    ct state established,related accept
    ct state invalid drop
    meta l4proto { icmp, ipv6-icmp } accept

    # WireGuard handshake from the internet
    iif "enp1s0" udp dport 51820 accept

    # Router administration only from management or the VPN
    iif { "vlan10", "wg0" } tcp dport 22 accept

    # DNS and DHCP for local segments and VPN clients
    iif { "vlan10", "vlan20", "vlan30", "vlan40", "wg0" } udp dport { 53, 67 } accept
    iif { "vlan10", "vlan20", "vlan30", "vlan40", "wg0" } tcp dport 53 accept
  }

  chain forward {
    type filter hook forward priority 0; policy drop;

    ct state established,related accept
    ct state invalid drop

    # Internet access for every local segment
    iif { "vlan10", "vlan20", "vlan30", "vlan40" } oif "enp1s0" accept

    # VPN clients reach management and services
    iif "wg0" oif { "vlan10", "vlan20" } accept

    # Trusted clients reach only web ports on the services segment
    iif "vlan30" oif "vlan20" tcp dport { 80, 443, 8080 } accept

    # Log what gets dropped, rate-limited so logs stay readable
    limit rate 5/minute log prefix "fwd-drop: "
  }
}

table ip nat {
  chain postrouting {
    type nat hook postrouting priority srcnat; policy accept;
    oif "enp1s0" masquerade
  }
}

Check the syntax before loading it, then enable the service so the rules survive a reboot:

sudo nft -c -f /etc/nftables.conf
sudo systemctl enable --now nftables

Notice that IoT (VLAN 40) has no forward rule except internet access, so it cannot reach any internal segment. Services (VLAN 20) can answer trusted clients but cannot initiate anything toward them, because the reply path is covered by the established state rule.

Step 4: Build the WireGuard server on the router

Install WireGuard and generate a key pair for the server. Restrict permissions so the private key is never world-readable:

sudo apt install wireguard
umask 077
wg genkey | sudo tee /etc/wireguard/server.key | wg pubkey | sudo tee /etc/wireguard/server.pub

Create /etc/wireguard/wg0.conf. Replace SERVER_PRIVATE_KEY with the contents of server.key and CLIENT_PUBLIC_KEY with the public key you generate for each client in the next step:

[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = SERVER_PRIVATE_KEY

[Peer]
# laptop
PublicKey = CLIENT_PUBLIC_KEY
AllowedIPs = 10.8.0.2/32

Bring the tunnel up and make it start at boot:

sudo systemctl enable --now wg-quick@wg0
sudo wg show

The wg0 interface is now part of the firewall rules above, so VPN clients are treated as a distinct, deliberately limited zone.

Step 5: Configure a client with a split tunnel

On the client, generate its own key pair, then add its public key to the server’s [Peer] block as shown in Step 4. The client configuration looks like this:

[Interface]
PrivateKey = CLIENT_PRIVATE_KEY
Address = 10.8.0.2/32
DNS = 10.8.0.1

[Peer]
PublicKey = SERVER_PUBLIC_KEY
Endpoint = YOUR_DOMAIN:51820
AllowedIPs = 10.8.0.0/24, 10.10.10.0/24, 10.20.0.0/24
PersistentKeepalive = 25

The AllowedIPs line is a split tunnel. Only lab subnets go through the VPN, and ordinary internet traffic stays on the local connection. Avoid 0.0.0.0/0 unless you want all client traffic routed through your home connection. If you do not run a resolver on the router, remove the DNS line. If you do run one, make sure it listens on 10.8.0.1 as well.

Step 6: Remove exposure and keep services internal

Make sure nothing on your ISP gateway or the router forwards ports to internal hosts. Any port forward you created earlier is now a bypass of the firewall, so delete it. Then check what your services are listening on:

sudo ss -tlnp

Services should bind to their segment address, such as 10.20.0.10, or to all addresses only on hosts that sit behind the firewall. Do not put a reverse proxy on the WAN side. Access to web applications should go through the VPN, and the firewall rules should allow only the ports you intend.

Verify it worked

Test from the outside first. Turn off WireGuard on a client and use a phone on cellular data, or any network outside your home:

nmap -sU -p 51820 YOUR_PUBLIC_IP
nmap -Pn -p 22,80,443,8080 YOUR_PUBLIC_IP

The first command should report the WireGuard port as open|filtered. WireGuard stays silent toward unauthenticated packets, so this is expected. The second command should show every TCP port as filtered or closed.

Next, test from the VPN. Turn WireGuard on and confirm you can reach a service:

ping -c 3 10.20.0.10
curl -I -m 5 http://10.20.0.10:8080

Then test segmentation. From a device on the IoT VLAN, this should time out:

curl -m 5 http://10.20.0.10:8080

On the router, confirm the policy is what you wrote and watch for blocked traffic:

sudo nft list ruleset
sudo wg show
sudo journalctl -k | grep fwd-drop

The fwd-drop entries should show the IoT device attempting to reach internal addresses. If your counters and logs match the policy, the lab is segmented and reachable only through the tunnel.

Common mistakes

  • Leaving IP forwarding off. The VLANs will appear to work for local traffic, but routing between segments and to the internet will silently fail. Confirm with sysctl net.ipv4.ip_forward.
  • Keeping the old port forwards. A forward left on the ISP gateway or the router bypasses the entire design. Audit both devices.
  • Locking yourself out. Apply firewall changes from the console or with a second session already open. Use netplan try for network changes.
  • Forgetting nftables persistence. Rules loaded by hand disappear on reboot. Enable the service and test a reboot.
  • Using a full-tunnel AllowedIPs by accident. Setting 0.0.0.0/0 on a client routes all of its traffic through your home link. Keep the split tunnel unless you intend otherwise.
  • Ignoring IPv6. The rules above filter IPv4 forwarding only. If your ISP gives you IPv6, devices may reach each other or the internet over IPv6 without passing through your IPv4 policy. Either configure IPv6 deliberately with matching rules or disable it on the segments.
  • Leaving access ports in the wrong VLAN. A device plugged into the wrong untagged port lands in the wrong segment. Label your ports and check each device after you move it.
  • Trusting the IoT segment with credentials. Segmentation limits the blast radius, but do not give IoT devices access to services that hold secrets, and keep their firmware updated.

Takeaway

A segmented home lab with a default-deny firewall and WireGuard as the only way in is a practical, low-cost improvement. You do not need enterprise gear. You need a clear address plan, a small firewall ruleset you can read in one sitting, and a VPN that answers only to authenticated peers. Once this is in place, adding a new service means choosing the segment it belongs in and writing one explicit rule, not reopening the network to the internet.