A home lab tends to grow in a messy way. A file server sits next to a smart plug, a media box shares a subnet with a laptop running untrusted software, and a dashboard you wrote last month is quietly reachable from every device on the network. The usual fix, port forwarding a few services to the internet, creates more risk than it solves. This guide shows a better pattern: split the network into VLANs, enforce default-deny firewall rules between them, and reach the lab through a single WireGuard port. When you are done, you can manage everything from your phone or laptop anywhere, and the only thing listening on your public address is the VPN.
This guide is for home-lab operators and self-hosters who are comfortable with a Linux command line and have a router or switch that supports 802.1Q VLAN tagging. The examples use a Linux gateway with nftables, but the principles apply to any firewall.
Before you start
- A Linux machine to act as the gateway (Debian, Ubuntu, or similar) with two network interfaces: one facing your upstream internet connection (called
eth0below) and one facing your internal switch (calledeno1below). - A managed switch that supports VLAN tagging, or a router with VLAN-aware ports. You need to be able to set one trunk port and several access ports.
- Root or sudo access on the gateway and a console or out-of-band way to reach it (a monitor, a serial console, or a second management path). Firewall and network changes can lock you out over SSH.
- A public IP address or a dynamic DNS name for your connection. Replace
YOUR_DOMAINwith that name in the examples. - WireGuard tools and nftables installed on the gateway:
sudo apt install wireguard nftables. - A list of the devices you plan to host, sorted into servers, IoT, and trusted clients.
Step 1: Plan your address space
Decide on the subnets before you touch any configuration. Keeping the plan on paper (or in a text file) prevents most of the confusion later. This guide uses four networks:
- VLAN 10, servers: 10.10.10.0/24, for your self-hosted services. The gateway is 10.10.10.1.
- VLAN 20, IoT: 10.10.20.0/24, for cameras, plugs, speakers, and anything you do not fully control. The gateway is 10.10.20.1.
- VLAN 30, trusted clients: 10.10.30.0/24, for your laptops and workstations. The gateway is 10.10.30.1.
- WireGuard tunnel: 10.99.0.0/24, used only for VPN peers. The server is 10.99.0.1.
Make sure these ranges do not overlap with the network your ISP or hotel Wi-Fi hands out. Overlapping subnets cause remote access to fail in confusing ways.
Step 2: Create the VLAN interfaces on the gateway
Each VLAN becomes a virtual interface on top of the physical trunk port. The gateway holds the address for each VLAN, so it routes between them and becomes the single place where you enforce policy. On a netplan-based system (Ubuntu and many others), create /etc/netplan/60-vlans.yaml:
network:
version: 2
ethernets:
eno1:
dhcp4: false
vlans:
eno1.10:
id: 10
link: eno1
addresses: [10.10.10.1/24]
eno1.20:
id: 20
link: eno1
addresses: [10.10.20.1/24]
eno1.30:
id: 30
link: eno1
addresses: [10.10.30.1/24]Apply it with sudo netplan apply, then confirm the tags are present:
ip -d link show eno1.20The output should show vlan protocol 802.1Q id 20. Notice that the physical interface eno1 carries no IP address. All routing happens on the tagged sub-interfaces.
Step 3: Tag ports on the switch
Switch configuration differs by vendor, but the logic is the same everywhere:
- Trunk port (the port connected to the gateway’s
eno1): tagged members for VLANs 10, 20, and 30. Leave no untagged VLAN on this port, or set an unused native VLAN, so untagged frames cannot leak into a network by accident. - Access ports for IoT devices: untagged, VLAN 20 only.
- Access ports for servers: untagged, VLAN 10 only.
- Access ports for workstations: untagged, VLAN 30 only.
Remember that a switch only separates traffic at layer 2. It cannot decide whether VLAN 20 may talk to VLAN 10. That decision belongs to the gateway, which is why Step 6 matters so much.
Step 4: Generate WireGuard keys and configure the server
WireGuard uses public key cryptography, so each side needs a key pair. Generate the server keys with a restrictive umask so the private key is never world-readable:
sudo sh -c 'umask 077; wg genkey | tee /etc/wireguard/server.key | wg pubkey > /etc/wireguard/server.pub'Create /etc/wireguard/wg0.conf on the gateway. Replace the placeholders with the real values:
[Interface]
Address = 10.99.0.1/24
ListenPort = 51820
PrivateKey = SERVER_PRIVATE_KEY
[Peer]
# laptop
PublicKey = CLIENT_PUBLIC_KEY
AllowedIPs = 10.99.0.2/32Lock down the file and enable IP forwarding, since the gateway must route packets between the tunnel and your VLANs:
sudo chmod 600 /etc/wireguard/wg0.conf
echo 'net.ipv4.ip_forward=1' | sudo tee /etc/sysctl.d/99-forward.conf
sudo sysctl --system
sudo systemctl enable --now wg-quick@wg0Each peer gets a unique AllowedIPs address. Keep the tunnel addresses separate from your LAN addresses so you can write precise firewall rules later.
Step 5: Configure the client
On each client device, generate a key pair the same way (run this on the client, not the server), then send only the public key to the server. Never copy a private key between machines.
umask 077; wg genkey | tee client.key | wg pubkey > client.pubCreate the client configuration, for example wg0-home.conf:
[Interface]
PrivateKey = CLIENT_PRIVATE_KEY
Address = 10.99.0.2/32
[Peer]
PublicKey = SERVER_PUBLIC_KEY
Endpoint = YOUR_DOMAIN:51820
AllowedIPs = 10.10.10.0/24, 10.10.30.0/24, 10.99.0.0/24
PersistentKeepalive = 25This is a split tunnel. Only traffic destined for the server and trusted VLANs goes through the VPN, while everything else uses your normal connection. Note that AllowedIPs controls routing on the client, not security. It decides what gets sent into the tunnel, but the gateway’s firewall decides what is actually permitted. The PersistentKeepalive line helps clients behind NAT keep the tunnel open. If you want all traffic through the lab, replace the list with 0.0.0.0/0, but only after you have confirmed the tunnel works.
Step 6: Write default-deny nftables rules
This is the core of the setup. Default-deny means every chain drops traffic unless a rule explicitly allows it. Create /etc/nftables.conf on the gateway:
#!/usr/sbin/nft -f
flush ruleset
define WAN = "eth0"
define WG = "wg0"
define SRV = "eno1.10"
define IOT = "eno1.20"
define TRUST = "eno1.30"
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
ct state established,related accept
ct state invalid drop
iif "lo" accept
iifname { $WG, $TRUST, $SRV, $IOT } icmp type echo-request accept
iifname $WAN udp dport 51820 accept
iifname { $WG, $TRUST } tcp dport 22 accept
}
chain forward {
type filter hook forward priority 0; policy drop;
ct state established,related accept
ct state invalid drop
iifname { $WG, $TRUST } oifname $SRV accept
iifname { $WG, $TRUST } oifname $IOT accept
iifname { $WG, $TRUST } oifname $TRUST accept
iifname { $WG, $TRUST } oifname $WAN accept
iifname $SRV oifname $WAN accept
iifname $IOT oifname $WAN accept
}
}
table ip nat {
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
oifname $WAN masquerade
}
}Read the rules as a list of exceptions to the drop policy:
- The input chain protects the gateway itself. Only WireGuard arrives from the WAN side, and SSH is allowed only from the tunnel and trusted VLAN.
- The forward chain controls traffic passing through. VPN clients and trusted workstations may reach the servers, IoT devices, and other trusted hosts. Servers and IoT devices may reach the internet. IoT devices have no path to the server or trusted VLANs, so a compromised camera cannot scan your file server.
- The nat table masquerades outbound traffic leaving through the WAN, so internal addresses are never routed on the public internet. There is no DNAT (port forwarding) rule anywhere, which means nothing inside the lab is reachable from outside unless you add it.
Because the table is inet, the drop policies also apply to IPv6. This ruleset does not allow IPv6 traffic, so if your ISP provides IPv6, either add equivalent rules or disable IPv6 on the lab interfaces before you rely on this configuration. Also note that if you run DHCP or DNS on the gateway itself, you need explicit input rules for those services on each VLAN.
Check the syntax before loading it, then apply and enable it at boot:
sudo nft -c -f /etc/nftables.conf
sudo nft -f /etc/nftables.conf
sudo systemctl enable --now nftablesThe -c flag performs a dry-run parse. Always run it before applying, and keep your console session open while you test.
Step 7: Forward only the WireGuard port
If your gateway sits behind an upstream router or modem, forward a single UDP port to it: UDP 51820, pointed at the gateway’s WAN address. Do not forward TCP anything. If the gateway is itself the edge device, there is nothing to forward. Its WAN interface receives the packets directly, and the input rule above is the only thing exposing the lab.
WireGuard is designed to stay silent toward anyone who does not hold a valid key. A port scanner will not get a banner or an error response, which is part of why this approach is so much quieter than exposing SSH or a web panel.
Verify it worked
Test each boundary deliberately rather than assuming the configuration is correct.
- Confirm the tunnel is up. On the gateway, run
sudo wg show. Under your peer you should see a recent “latest handshake” time and non-zero transfer counters. Then from the client, runping -c 3 10.10.10.10(or any server address on your network). - Confirm trusted access works. From the VPN client, SSH to a server on VLAN 10 and open any web service you host there. Access should work without any port forwarding.
- Confirm IoT isolation. From a device on VLAN 20, try
ping -c 3 10.10.10.10andping -c 3 10.10.30.1. Both should fail. Then runcurl -m 5 https://example.com, which should succeed, showing that the device can reach the internet but nothing else. - Confirm the gateway is locked down. From the WAN side (a phone on mobile data is a good test), scan your own public address:
nmap -Pn -p- YOUR_DOMAIN. You should see no open TCP ports. Then runsudo nmap -sU -p 51820 YOUR_DOMAIN. The result should be open or filtered, with no other services visible. - Confirm the rules are live. Run
sudo nft list rulesetand check that the policies readpolicy dropon the input and forward chains. Runsysctl net.ipv4.ip_forwardand confirm it returns1. - Confirm everything survives a reboot. Run
sudo reboot, then repeat the WireGuard and nftables checks above. Configuration that works until the next restart is not finished.
Common mistakes
- Locking yourself out. Applying a default-drop input policy over SSH without an existing rule for your session is the most common error. Keep console access available and apply changes with
nft -cfirst. - Forgetting IP forwarding. The tunnel comes up and handshakes succeed, but nothing beyond the gateway responds. Check
net.ipv4.ip_forwardfirst. - Treating AllowedIPs as a firewall. Client-side
AllowedIPsonly controls what enters the tunnel. Any peer with a valid key can send traffic to the gateway, so the nftables rules remain the real control. - Forwarding more than one port. Every extra forwarded port is attack surface. Keep the WAN side to UDP 51820 and nothing else.
- Leaving IPv6 open by accident. A working IPv4 firewall can be undermined by an unfiltered IPv6 path. Confirm IPv6 behavior before trusting the lab boundary.
- Assuming VLANs filter traffic. VLANs separate broadcast domains, but routing between them is what your gateway does. If the gateway forwards everything, the VLANs provide little protection.
- Misconfigured trunk ports. An untagged or native VLAN left on the trunk can put devices into the wrong network silently. Verify each access port with a test device after you change the switch.
- Forgetting to revoke peers. When you retire a device, remove it from the server immediately with
sudo wg set wg0 peer CLIENT_PUBLIC_KEY remove, then delete the matching[Peer]block fromwg0.confso it does not return after a restart.
Takeaway
A segmented home lab is less about any single tool and more about discipline: separate networks by trust level, deny everything by default, and expose exactly one door. WireGuard gives you a small, quiet entry point, VLANs give you structure, and nftables turns your trust decisions into rules you can test. Once the boundary works, add services behind it with the same question every time: which network needs to reach this, and on which port?
